lambda只适用于方法参数列表是函数式接口的情况,函数式接口即接口中有且仅有一个抽象方法的接口【lambda表达式相当于对函数式接口抽象方法的重写】
老的匿名内部类编译以后的字节码文件,发现匿名内部类实际上编译会生成实现对应接口的一个新的类,类的名称为接口名$1
使用了Lambda后
XJad
反编译工具无法反编译 ,可以使用jdk自带的工具javap对使用了lambda的类的字节码文件进行反汇编,使用方法是在DOS命令行输入javap -c -p 文件名.class
Lambda表达式会在当前类中生成一个名为lambda$上级方法名如main$0
的私有静态方法,lambda表达式中的代码会放在这个新增的方法的方法体中,在运行的时候会在方法中生成一个名为lambda表达式所在的类$$Lambda$1
的匿名内部类并重写其中的抽象方法,重写方法的时候会在重写方法中调用lambda表达式中生成的私有静态方法lambda$上级方法名如main$0
,即lambda表达式会生成匿名内部类和lambda表达式中写入方法对应的静态方法
运行时生成的类可以通过命令
java -Djdk.internal.lambda.dumpProxyClasses 要运行的包名.类名
将运行时生成的内部类class码输出到一个文件中lambda表达式就是对函数式接口的实现并且重写其中的抽象方法
方法参数或者局部变量的类型必须是函数式接口才能使用Lambda表达式
其他参数类型或者参数类型是抽象类都不能使用Lambda表达式
局部变量单独定义时也可以定义成匿名内部类的形式,这种情况也可以把匿名内部类写成Lambda表达式的结构
函数式接口:有且只有一个抽象方法的接口,接口上使用注解
@FunctionalInterface
能够检测接口中是否只含有一个抽象方法
Flyable f=()->{
System.out.println("i can fly")
}
Lambda表达式和匿名内部类使用上的区别
匿名内部类实现的可以是类、抽象类、接口;Lambda表达式需要的类型必须是函数式接口
匿名内部类实例
Person
是现存的一个类,new Person(){}
的意思是创建一个匿名内部类继承Person类,并实例化成一个对象,抽象类中没有抽象方法可以不重写,有抽象方法需要重写
xxxxxxxxxx
public static void test(){
new Person(){
}
}
匿名内部类实现的类、抽象类、接口中抽象方法的数量随意;Lambda表达式实现的只能是函数式接口
匿名内部类在编译后生成单独的class,Lambda表达式在程序运行时动态生成匿名内部类
标准格式
参数列表是
参数类型 参数值,参数类型 参数值,...
,函数不需要参数只写括号lambda表达式是对函数式接口中的抽象方法的视线,用在方法需要函数式接口实例的情况下,直接写Lambda表达式相当于写了实现了函数式接口并创建了一个匿名内部类并实现了抽象方法,该实例可以在方法中执行实例对应的抽象方法
箭头的作用是分割参数和方法体
【语法】
xxxxxxxxxx
(参数列表)->{
方法体
}
【实例】
xxxxxxxxxx
(int a,String b)->{
return new Persion(a,b);
}
省略格式
参数列表的参数类型可以省略
xxxxxxxxxx
(a,b)->{
return new Persion(a,b);
}
如果参数列表只有一个参数,参数类型和括号都可以省略
xxxxxxxxxx
a->{
return new Persion(a);
}
如果大括号内有且只有一个语句,可以同时省略大括号、return和语句分号
以上省略大括号、return和语句分号三个部分必须同时省略
xxxxxxxxxx
a->new Persion(a)
内置函数式接口主要在
java.util.function
包下
方法声明时可以直接对未实现的抽象方法进行调用和传参
xxxxxxxxxx
public class Test01 {
public static void main(String[] args) {
test((a,b)->a+b);
}
public static void test(Operation op){
//可以对未实现的抽象方法进行调用和传参
int sum = op.getSum(1, 2);
System.out.println(sum);
}
}
interface Operation{
public abstract int getSum(int a,int b);
}
常用的内置函数式接口
Supplier接口
供给型接口,不给参数但是返回返回值的接口,可以对当前方法括号外的局部变量或者成员变量进行操作
xxxxxxxxxx
public interface Supplier<T> {
public abstract T get();
}
应用举例
打断点查看执行过程会发现先跳到printMax方法中执行
supplier.get()
前的方法,执行到supplier.get()
方法才会跳到Lambda表达式中去执行Lambda表达式的代码
xxxxxxxxxx
public class Test02 {
//使用函数式接口找出数组中最大的元素,感觉这种写法很容易写成脱了裤子放屁的感觉
public static void main(String[] args) {
int[] arr={11,99,77,88,22};
printMax(()->{
Arrays.sort(arr);
return arr[arr.length-1];
});
}
//声明函数式接口时Supplier<Integer>规定了函数式接口的返回值的类型为Integer
public static void printMax(Supplier<Integer> supplier){
Integer max = supplier.get();
System.out.println("最大的数:"+max);
}
}
Consumer接口
消费型接口,需要参数,但是没有返回值
只需要使用传入的参数但是不用返回返回值,比如对数组排序,将字符串转换成大写或者小写
x
public interface Consumer<T> {
void accept(T t);
default Consumer<T> andThen(Consumer<? super T> after) {
Objects.requireNonNull(after);
//这里完整写法是return Consumer<T> (T t) -> { accept(t); after.accept(t); },实际上是返回一个Consumer<T>类型的匿名内部类,相当于用Lambda表达式重写了一个自定义Consumer<T>的实现类,并实现了其中的accept方法,但是特别注意,第一个accept(t)在Lambda表达式的底层是写在匿名内部类c1的实现类中的静态方法[因为通过c1调用的andThen方法],这个accept方法是写在该静态方法中的,调用的时候自然是调用c1实例对应的accept方法,即c1实例用lambda表达式实现的方法,然后再自然执行c2.accept(t)方法
return (T t) -> { accept(t); after.accept(t); };
}
}
应用举例
xxxxxxxxxx
public class Test03 {
public static void main(String[] args) {
String str="Hello World!";
printInCases(s->System.out.println(s.toUpperCase()),
s->System.out.println(s.toLowerCase()),
str);
}
//引用同一个函数式接口两次实现分别对字符串进行全体大写处理和全体小写处理,并通过andThen方法实现两个accept方法的调用
public static void printInCases(Consumer<String> c1,Consumer<String> c2,String str){
//c1.accept(str);
//c2.accept(str);
c1.andThen(c2).accept(str);
}
}
Function接口
传入一个参数,经过加工以后返回另一个数据
这个地方结合之前学的super和泛型好好看一下,实际课堂只讲了apply和andThen,andThen的逻辑和Consumer是一样的
xxxxxxxxxx
public interface Function<T, R> {
R apply(T t);
//形参中使用了泛型需要在方法名之前进行声明,Function<V, R>是一种声明方式,<V>也是一种声明方式,疑问,为啥Function<V, R>声明了还要单独声明一次<V>
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
}
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}
static <T> Function<T, T> identity() {
return t -> t;
}
}
应用实例
xxxxxxxxxx
public class Test04 {
//将字符串转成数字,然后数字乘以5
public static void main(String[] args) {
String str="7";
getNumber(strData->Integer.parseInt(strData),intData->5*intData,str);
}
//Function<String,Integer>表示apply方法需要String类型参数,返回Integer类型的参数
public static void getNumber(Function<String,Integer> f1,Function<Integer,Integer> f2,String str){
//Integer count = f1.apply(str);
//Integer result = f2.apply(count);
Integer result = f1.andThen(f2).apply(str);
System.out.println("最终结果:"+result);
}
}
Predicate接口
传入一个数据,根据逻辑对参数进行判断,返回true或者false;判断型接口
xxxxxxxxxx
public interface Predicate<T> {
boolean test(T t);
//多个判断同时满足就返回true
default Predicate<T> and(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) && other.test(t);
}
//判断一个字符串不满足某个判断就返回true
default Predicate<T> negate() {
return (t) -> !test(t);
}
//只要满足多个条件中的一个就返回true
default Predicate<T> or(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) || other.test(t);
}
static <T> Predicate<T> isEqual(Object targetRef) {
return (null == targetRef)
? Objects::isNull
: object -> targetRef.equals(object);
}
"unchecked") (
static <T> Predicate<T> not(Predicate<? super T> target) {
Objects.requireNonNull(target);
return (Predicate<T>)target.negate();
}
}
应用实例
xxxxxxxxxx
public class Test05 {
//判断字符串中是否包含H字符和W字符
//以及用Predicate中的API实现多种判断组合
public static void main(String[] args) {
String str="Hello World!";
//str.contains("W")区分大小写
test(data->data.contains("W"),data->data.contains("H"),str);
}
public static void test(Predicate<String> p1,Predicate<String> p2,String str){
//boolean containW = p1.test(str);
//boolean containH = p2.test(str);
//if(containW && containH){
// System.out.println("同时包含W和H");
//}else if (containW){
// System.out.println("包含W");
//}else if(containH){
// System.out.println("包含H");
//}else{
// System.out.println("都不包含");
//}
boolean test = p1.and(p2).test(str);
System.out.println(test?"同时包含W和H":"不同时包含W和H");
boolean test1 = p1.or(p2).test(str);
System.out.println(test1?"至少包含W和H中的一个":"两个字符都不包含");
boolean test2 = p1.negate().test(str);
System.out.println(test2?"不包含W字符":"包含字符");
}
}
lambda表达式仍然可能存在冗余的场景【lambda表达式的方法体直接调用的现成的方法体,显得比较冗余】,结合方法引用可以进一步的简化Lambda表达式
方法引用在Lambda表达式中的实例
方法引用的方法的参数列表需要和函数式接口的抽象方法的参数列表保持一致
被引用方法与抽象方法需要有相同类型的返回值
以上两个条件不满足编译就会报错
xxxxxxxxxx
public class Test06 {
public static void main(String[] args) {
//冗余写法
printSum(()->{
int[] arr={1,2,3,4,5,6};
int sum=0;
for (int data: arr) {
sum+=data;
}
return sum;
});
//方法引用,传参需要在printSum方法中传递,如果抽象方法需要使用参数对抽象方法的调用处传递参数给抽象方法
// 方法引用是不能传递参数的,因为实现抽象方法只需要知晓参数类型和参数个数,不需要知道具体参数,
// 同时就限定了函数式接口抽象方法的参数列表应该和方法引用保持一致
// 这种方式限制死了变量要通过外层函数printSum来传递,方法引用的参数列表需要和函数式接口的抽象方法的参数列表保持一致
// 同时也限制死了被引用方法与抽象方法需要有相同类型的返回值
//printSum(Test06::getSum);
}
public static Integer getSum(){
int[] arr={1,2,3,4,5,6};
int sum=0;
for (int data: arr) {
sum+=data;
}
return sum;
}
private static void printSum(Supplier<Integer> supplier){
//参数引用传递参数必须通过printSum函数将从主函数传递来的参数通过下面对抽象方法的调用的同时传递参数给抽象方法
System.out.println("数组的和为:"+supplier.get());
}
}
常见方法引用的方式
除去对静态方法的方法引用外还有多种其他方法引用方式,列举如下
instanceName::methodName
对象::实例方法名
实例
xxxxxxxxxx
//非maven项目引入junit依赖需要在本地仓库找到junit.junit-4.12和org.hamcrest.hamcrest-core-1.3,同时导入才不会报错
//同时需要添加测试目录才能生效
public void test(){
Date now=new Date();
//冗余写法
//Supplier<Long> supplier=()-> now.getTime();
//对实例方法进行方法引用
Supplier<Long> supplier=now::getTime;
System.out.println("当前时间毫秒数:"+supplier.get());
}
ClassName::staticMethodName
类名::静态方法
实例
xxxxxxxxxx
//类名::静态方法
public void test02(){
//冗余写法
Supplier<Long> supplier=()->System.currentTimeMillis();
//对静态方法进行方法引用
//Supplier<Long> supplier=System::currentTimeMillis;
System.out.println("当前时间毫秒数~"+supplier.get());
}
ClassName::methodName
类名::实例方法名
平常使用的时候,类名是无法直接调用实例方法的,Lambda表达式对这种调用方式进行了特殊处理,使用
类名::实例方法名
进行方法引用时在方法调用的时候会自动将第一个参数作为方法的调用者,即本质上还是通过对象去调用的实例方法,调用时后续参数才是参数列表的内容,泛型对应第一个是对象的类型、参数列表、返回值类型疑问:函数式接口中的抽象方法能被重载吗
实例
xxxxxxxxxx
//类名::实例方法
public void test03(){
//冗余写法
//使用Function接口实现,不知道为什么要举BiFunction的例子,感觉是用BiFunction来扩展需要传参的方式,这里我理解成
//接口中已经限制了抽象方法的参数个数,自定义函数式接口也需要规定泛型和抽象方法的参数个数,因此参数个数不同或者参数列表
// 不同就需要使用不同的函数式接口
//感觉像类名对对应接口专门进行了处理
//Function<String,Integer> f1=(str)->str.length();
//Integer strLength = f1.apply("hello");
//使用类名对实例方法进行方法引用
Function<String,Integer> f1=String::length;
Integer strLength = f1.apply("hello");
System.out.println("length为"+strLength);//length为5
//冗余写法
//使用BiFunction处理可以传递两个参数的获取一个返回值的接口,实际上这里使用Function接口和BiFunction接口的区别就是
// 使用类名调用实例方法本身就需要消耗一个参数作为方法调用的对象,如果需要传递参数就对接口中的抽象方法的参数列表有限制,
// 此时就需要使用不同的接口,函数式接口一般都对抽象方法的参数类型和返回值类型做了泛型规范,所以一般在接口定义中就对参
// 数个数进行了限制,所以这里如果把BiFunction改成Function就会报错
//BiFunction<String,Integer,String> f2=(str, index)->str.substring(index);
//方法引用
BiFunction<String,Integer,String> f2=String::substring;
String handleStr = f2.apply("HelloWorld", 3);
System.out.println("处理后的String:"+handleStr);//处理后的String:loWorld
}
ClassName::new
类名::new 引用类的构造器
用
类名::new
的方式引用类的构造器,这种引用方式调用类的无参构造还是有参数构造主要看函数式接口的泛型数量和抽象方法的传参个数,暂时把抽象方法的参数列表看成对应构造方法的重载,参数列表就是对应构造器的参数列表
实例
xxxxxxxxxx
public class TestLambda {
public void test04(){
//无参数构造使用不需要传参但是有返回值的函数式接口,Supplier就是这样特征的函数式接口
//冗余写法
//Supplier<Person> supplier1=()->new Person();
//构造器方法引用
Supplier<Person> supplier1=Person::new ;
supplier1.get(); //Person的无参数构造方法执行
//有参数构造方法
// 冗余写法
BiFunction<String,Integer,Person> bif=(str,age)->new Person(str,age);
Person user = bif.apply("张三", 25); //Person的有参数构造方法执行
}
}
class Person{
private String name;
private Integer age;
public Person() {
System.out.println("Person的无参数构造方法执行");
}
public Person(String name, Integer age) {
System.out.println("Person的有参数构造方法执行");
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
TypeName[]::new
String[]::new 调用数组的构造器
int[]和String[]也可以看做一个类
实例
xxxxxxxxxx
//int[]::new 引用数组的构造器
public void test05(){
//冗余写法
//Function<Integer,int[]> f1=(length)->new int[length];
//方法引用
Function<Integer,int[]> f1=int[]::new;
int[] arr = f1.apply(10);
System.out.println(Arrays.toString(arr));
}
Stream流和IO流没有任何关系,流式思想类似于工厂车间的生产流水线,Stream不是一种数据结构,不保存数据,而是对数据进行加工处理,Stream可以看做是流水线上的一个工序,在流水线上通过多个工序让一个原材料加工成一个商品
一般的Stream流都是单线程的,效率不够高;还有并行的Stream流
案例初体验
实例
xxxxxxxxxx
//集合的初次体验
public void testFilter(){
//准备一个集合,初始化数据
ArrayList<String> userList = new ArrayList<>();
Collections.addAll(userList,"张无忌","周芷若","赵敏","张三","张三丰");
//传统的对集合元素依次进行下列操作,获取到姓张的,名字长度为3个字的,并打印这些数据
ArrayList<String> zhangList = new ArrayList<>();
for (String user:userList) {
if (user.startsWith("张")) {
zhangList.add(user);
}
}
ArrayList<String> threeList = new ArrayList<>();
for (String user:zhangList) {
if (user.length()==3) {
threeList.add(user);
}
}
for (String user:threeList) {
System.out.print(user);
}
System.out.println();
//Stream流式操作,filter需要传参函数式接口Predicate的实现类
userList.stream().filter((str)->str.startsWith("张")).filter((str->str.length()==3)).forEach(str-> System.out.print(str));
}
通过Collection接口中的默认方法default Stream<E> stream()
获取Stream流
返回值是Stream类型
Map接口和Collection接口没有什么关系,所以Map接口没有stream方法,Map接口可以通过
map.keySet()
方法得到key的set集合可以使用stream方法;
map.value()
方法可以得到所有value值并放入Collection集合中使用stream方法得到stream流
map.entrySet()
方法可以得到键值对的set集合也可以通过stream方法得到stream流
通过Stream
中的静态方法static<T> Stream<T> of(T... values)
也可以获取流
可变长度参数的本质是一个数组,
实例
xxxxxxxxxx
Stream<String> stream=Stream.of("aa","bb","cc");
//可变参数列表可以传入数组
String[] strs={"aa","bb","cc"};
Stream<String> stream2=Stream.of(strs);
//基本数据类型的数组虽然写出来不会报错,但是Stream流会将整个数组看做一个元素进行操作,这样是会出问题的,实际应用不要写
int[] arr={11,22,33};
Stream<int[]> stream3=Stream.of(arr);
根据返回结果Stream流的方法可以分为终结方法和非终结方法,区分标准就是非终结方法的返回值类型是Stream,而终结方法的返回值类型不是Stream,可能是long或者void;非终结方法也叫函数拼接方法,即返回值仍然是Stream本身,支持链式调用
Stream流的常用方法
不是全部,更多方法参考API文档
long-->stream.count()
作用:统计Stream流中元素的个数
实例:
xxxxxxxxxx
//stream.count()
public void testStreamAPI4(){
ArrayList<String> user = new ArrayList<>();
Collections.addAll(user,"迪丽热巴","宋远桥","苏星河","老子","庄子","孙子");
System.out.println(user.stream().count());
}
void-->stream.forEach(Consumer<? super String> action)
作用:遍历流中的数据,对流中的元素进行统一处理
实例:
xxxxxxxxxx
//stream.forEach()
public void testStreamAPI3(){
ArrayList<String> user = new ArrayList<>();
Collections.addAll(user,"迪丽热巴","宋远桥","苏星河","老子","庄子","孙子");
//得到流用流的forEach方法.println方法是类System.out中的静态方法,可以用方法引用
user.stream().forEach(str-> System.out.println(str));
//方法引用
user.stream().forEach(System.out::println);
}
Stream-->stream.filter(Perdicate perdicate)
作用:过滤掉一些元素,返回符合过滤条件的数据
实例:
xxxxxxxxxx
//stream.filter()
//Stream<T> filter(Predicate<? super T> predicate);
public void testStreamAPI5(){
ArrayList<String> user = new ArrayList<>();
Collections.addAll(user,"迪丽热巴","宋远桥","苏星河","老子","庄子","孙子");
//将名字长度为3个字的人过滤掉,必须要调用终结方法才能执行非终结方法
user.stream().filter(str->str.length()!=3).forEach(System.out::println);
}
Stream-->stream.limit(Integer count)
作用:取用流中的前几个元素
实例:
xxxxxxxxxx
//stream.limit(long maxSize)
public void testStreamAPI6(){
ArrayList<String> user = new ArrayList<>();
Collections.addAll(user,"迪丽热巴","宋远桥","苏星河","老子","庄子","孙子");
//使用limit方法,参数是指定要保留前多少个数据
user.stream().limit(3).forEach(System.out::print);//迪丽热巴宋远桥苏星河
}
Stream-->stream.skip(Integer count)
作用:跳过流中的前count个元素
实例:
xxxxxxxxxx
//stream.skip()
public void testStreamAPI7(){
ArrayList<String> user = new ArrayList<>();
Collections.addAll(user,"迪丽热巴","宋远桥","苏星河","老子","庄子","孙子");
//使用skip方法,参数是指定要跳过前多少个数据
user.stream().skip(3).forEach(System.out::print);//老子庄子孙子
}
Stream-->stream.map(Function function)
作用:映射,功能是将流中的元素类型转换成另外一种类型,比如字符串转换成Integer类型,同时还可以返回原类型,但是修改了对应元素的属性
备注说明:如果Function函数式接口实现是构造方法引用,会自动将元素作为参数调用有参数构造方法
实例:
xxxxxxxxxx
//stream.map()
public void testStreamAPI8(){
ArrayList<String> data = new ArrayList<>();
Collections.addAll(data,"11","22","33");
//使用map方法将String类型的元素转换成其他类型的元素,转换规则要在map的参数中的Function函数式接口中指定
//<R> Stream<R> map(Function<? super T, ? extends R> mapper);
data.stream().map(Integer::parseInt).forEach(System.out::print);//112233
}
Stream-->stream.sorted()
作用:无参sorted根据集合的自然顺序来排序,是字典序吗?
实例:
xxxxxxxxxx
//stream.sorted(),根据自然序对元素进行排序
public void testStreamAPI9(){
ArrayList<String> data = new ArrayList<>();
Collections.addAll(data,"33","22","11","55");
data.stream().sorted().forEach(System.out::print);//11223355
}
Stream-->stream.sorted(Comparator<? super T> comparator)
作用:根据比较器指定的规则排序,需要传参Comparator比较器
实例:
xxxxxxxxxx
//stream.sorted(Comparator<?>),根据指定排序规则对元素进行排序
//Comparator是函数式接口,只有一个抽象方法int compare(T o1, T o2);如果o1-O2就是升序,o2-o1就是降序
public void testStreamAPI10(){
ArrayList<String> data = new ArrayList<>();
Collections.addAll(data,"33","22","11","55");
data.stream().
map(Integer::parseInt).
//sorted((Integer ele1,Integer ele2)->(ele2-ele1)).
//简写需要参数类型本身就是Integer或者能加减的类型,如果本身就是自然序,排序是不用写的,写了也不会生效
sorted((ele1,ele2)->(ele2-ele1)).
forEach(System.out::print);//55332211
}
Stream-->stream.distinct()
作用:去掉流中重复的数据
备注说明:默认情况下是无法去掉自定义的重复数据类型,比如Person;distinct方法判断重复的依据是hashCode和equals方法,重写以后针对自定义类才有判断对象重复的依据
实例:
xxxxxxxxxx
//stream.distinct()流中重复的数据只保留一份
public void testStreamAPI11(){
//Stream<String> stream = Stream.of("11", "22", "33", "22", "33", "11");
//stream.distinct().forEach(System.out::print);//112233
//默认情况下重复的自定义类distinct方法不起效,需要重写自定义类的HashCode方法和equals方法
Stream<Person> person = Stream.of(new Person("貂蝉", 16),
new Person("杨玉环",18),
new Person("杨玉环",18),
new Person("西施",20),
new Person("西施",20),
new Person("王昭君",22));
person.distinct().forEach(System.out::println);
// 没重写自定义类的hashCode和equals方法,无法对自定义类去重
/**
* Person{name='貂蝉', age=16}
* Person{name='杨玉环', age=18}
* Person{name='杨玉环', age=18}
* Person{name='西施', age=20}
* Person{name='西施', age=20}
* Person{name='王昭君', age=22}
* */
//重写自定义类的hashCode和equals方法以后对自定义类去重
/**
* Person{name='貂蝉', age=16}
* Person{name='杨玉环', age=18}
* Person{name='西施', age=20}
* Person{name='王昭君', age=22}
* */
}
Stream-->stream.allMatch(Pewdicate<? super I> predicate)
作用:判断流中的元素是否全部满足指定条件,全部满足返回true,不全部满足返回false
实例:
xxxxxxxxxx
//allMatch()检查流中的数据是否完全匹配
public void testAllMatch(){
Stream<Integer> stream = Stream.of(5, 3, 6, 1);
boolean result = stream.allMatch((Integer i) -> {
return i > 0;
});
System.out.println(result);
}
Stream-->stream.anyMatch(Pewdicate<? super I> predicate)
作用:判断流中是否存在元素匹配指定条件,存在满足条件的元素返回true,不存在返回false
实例:
xxxxxxxxxx
//anyMatch()检查流中的元素是否存在匹配指定条件,存在返回false,不存在返回false
public void testAnyMatch(){
Stream<Integer> stream = Stream.of(5, 3, 6, 1);
boolean result = stream.anyMatch(i -> i > 6);
System.out.println(result);
}
Stream-->stream.noneMatch(Pewdicate<? super I> predicate)
作用:判断流中是否所有元素都不匹配指定条件,都不匹配返回true,存在元素匹配返回false
实例:
xxxxxxxxxx
//noneMatch()检查流中的元素是否都不匹配指定条件,都不匹配返回true,存在至少一个匹配就返回false
public void testNoneMatch(){
Stream<Integer> stream = Stream.of(5, 3, 6, 1);
boolean result = stream.noneMatch(i -> i < 0);
System.out.println(result);
}
Optional<Integer>-->stream.findFirst()
作用:找到流中的第一个元素
备注说明:返回值类型Optional<Integer>
,意思是该方法的结果可能找得到,也可能找不到,值需要使用option.get()
方法获取;注意Optional<Integer>-->stream.findAny()
的作用和findFirst()
一样都是找到第一个元素
实例:
xxxxxxxxxx
//findFirst()检查流中的元素是否都不匹配指定条件,都不匹配返回true,存在至少一个匹配就返回false
public void testFindFirst(){
Stream<Integer> stream = Stream.of(5, 3, 6, 1);
//Optional<Integer> firstElement = stream.findFirst();
Optional<Integer> firstElement = stream.findAny();//findAny和findFirst找到的都是第一个元素
System.out.println(firstElement.get());//5
}
Optional<Integer>-->stream.max(Comparator<? super Integer> comparator)
作用:找到流中值最大的元素
备注说明:Comparator接口抽象方法o1-o2
是升序【看成序号正常升就是升序】,配合Comparator接口其实可以实现单独使用max或者min方法同时获取最大值和最小值的需求
实例:
xxxxxxxxxx
//max()获取到流中元素的最大值,比较规则通过实现Comparator接口来实现
public void testMax(){
//o1-o2是元素按照升序排序
Optional<Integer> max = Stream.of(5, 3, 6, 1).max((o1, o2) -> o1 - o2);
System.out.println(max.get());
}
Optional<Integer>-->stream.min(Comparator<? super Integer> comparator)
作用:找到流中值最大的元素
实例:
xxxxxxxxxx
//min()获取到流元素中按照指定顺序排序的最小值
public void testMin(){
//Optional<Integer> min = Stream.of(5, 3, 6, 1).min((o1, o2) -> o1 - o2);
//System.out.println(min.get());//1
Optional<Integer> min = Stream.of(5, 3, 6, 1).min((o1, o2) -> o2 - o1);
System.out.println(min.get());//6
}
T-->stream.reduce(T identity,BinaryOperator<T> accumulator)
作用:找到流中值最大的元素
备注说明:Map方法和reduce方法组合使用能大大简化对集合的操作
xxxxxxxxxx
//reduce和map方法组合使用能大大简化集合开发
public void testMapReduce(){
//数据
Stream<Person> userList = Stream.of(
new Person("刘德华", 58),
new Person("张学友", 56),
new Person("郭富城", 54),
new Person("黎明", 52)
);
//求出所有年龄的总和:map得到年龄的流,对年龄求和
Integer ageSum = userList.map(user -> user.getAge())
//可以使用Integer内部求和的静态方法Integer.sum
//.reduce(0, (x, y) -> x + y);
.reduce(0,Integer::sum);
System.out.println("用户年龄总和:"+ageSum);
//获取到年龄的最大值
//流不能重复操作,以下对同一个流再次使用map方法会报错,再次使用map方法需要再次重新创建流
//Optional<Integer> maxAge = userList.map(user -> user.getAge()).reduce((x, y) -> x > y ? x : y);
Stream<Person> userList2 = Stream.of(
new Person("刘德华", 58),
new Person("张学友", 56),
new Person("郭富城", 54),
new Person("黎明", 52)
);
//哪个元素大返回哪个可以调用Math.max方法
//Optional<Integer> maxAge = userList2.map(user -> user.getAge()).reduce((x, y) -> x > y ? x : y);
Optional<Integer> maxAge = userList2.map(user -> user.getAge()).reduce(Math::max);
System.out.println("maxAge:"+maxAge.get());
//统计字符a出现的次数
Integer count = Stream.of("a", "c", "b", "a", "b", "a").map(a -> a == "a" ? 1 : 0).reduce(0, Integer::sum);
System.out.println("a字符出现的次数:"+count);
}
实例:
xxxxxxxxxx
//reduce()按照自定义的方式对流中的元素结合上次执行的结果自定义对元素进行操作
public void testReduce(){
//T reduce(T identity, BinaryOperator<T> accumulator);
//T identity:默认值
//BinaryOperator<T> accumulator:自定义对数据处理的方式,BinaryOperator接口继承自BiFunction接口,里面只有静态方法,所以这个
//函数式接口的抽象方法是继承的BiFunction的apply方法,这里对BinaryOperator<T>的实现其实是对BiFunction的apply方法的实现
//这儿BinaryOperator<T>中的泛型是根据reduce方法传参类型自动确定的,这个泛型限制死了BiFunction的三个泛型类型都是reduce的第一个参数
//的类型,即apply接口的两个参数和返回值需要和reduce的第一个参数的类型相同
//可以看出,x是前一次reduce方法执行得到的结果,y是每次按顺序选中的流中的元素,如果指定了初始值,x一开始就为初始值,y为流中的第一个元素
//如果没有指定初始值,x就以流中的第一个元素作为初始值,y以流中的第二个值作为初始值
Integer result = Stream.of(4, 5, 3, 9).reduce(0, (x, y) -> {
System.out.print("x="+x+",y="+y+";");//x=0,y=4;x=4,y=5;x=9,y=3;x=12,y=9;
return x + y;
});
System.out.println(result);//21
}
IntStream-->stream.mapToInt(ToIntFunction<? super T> mapper)
作用:可以将Stream<Integer>
中的Integer
类型转换成int类型
备注说明:使用包装类存在性能问题,Integer
占用的内存比int
多,Stream
流在操作过程中自动装箱拆箱;除了mapToInt
还有其他的mapToLong
等其他方法可以将元素转成其他的基本数据类型;Stream
、IntStream
、LongStream
都继承于BaseStream
,区别就是IntStream
相对于Stream
操作的是基本数据类型而不是包装类
实例:
xxxxxxxxxx
//mapToInt()能将Stream<Integer>类型的流转成基本数据类型int的流,减少自动装箱拆箱和内存开销
public void testNumericStream(){
//Integer占用的内存比int多,在Stream流中操作会自动装箱拆箱
//对于int类型的数据流会因为要使用泛型将int类型的数据自动装箱成Integer类型
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
//把大于3的元素打印出来,对数据进行比较操作的时候也会自动拆箱
stream.filter(n->n>3).forEach(System.out::println);
//IntStream mapToInt(ToIntFunction<? super T> mapper);将stream流中的包装类Integer
//IntStream内部操作的是int类型的数据,可以节省内存,减少自动装箱拆箱
//通过Integer的实例方法intValue能够将Integer类型转成int类型,这样能减少自动装箱拆箱以及内存开销
IntStream intStream = Stream.of(1, 2, 3, 4, 5).mapToInt(Integer::intValue);
intStream.filter(n->n>3).forEach(System.out::println);
}
Stream<T>-->Stream.concat(Stream<? extends T> a,Stream<? extends T> b)
作用:将两个Stream流合并成一个stream流,是将两个流中的元素
备注说明:被合并的两个stream流的元素类型是相同的,合并后的stream流的元素类型也是一样的;参与合并的流或者中间流不能使用forEach方法,会提示非法状态错误页
实例:
xxxxxxxxxx
//concat()方法能将两个流合并成一个流,合并是合并流中的元素到一个流中
public void testConcat(){
Stream<String> stream1 = Stream.of("张三","王五");
Stream<String> stream2 = Stream.of("李四","王五");
//这种合并方式只支持两个流合并,不支持三个及以上的流合并;同时合并以后不能操作之前的流,合并过程中有相同的元素也会完全保留到新流中
//但是可以多次合并,被合并的流和中间流不能进行其他流式操作,否则会抛非法状态异常;我这里对流使用了forEach会显示已经用过,
//猜测是因为合并过程需要使用forEach所以会提示重复,不知道其他方法是否能使用,有应用场景试一下
Stream<String> newStream = Stream.concat(stream1, stream2);
//newStream.forEach(System.out::println);
Stream<String> stream3 = Stream.of("赵六", "王五");
Stream<String> newStream2 = Stream.concat(newStream, stream3);
newStream2.forEach(System.out::println);
}
Stream流的注意事项
对一个Stream流的方法只能操作一次,重复相同操作会抛非法状态异常,如下所示这种情况就就会抛异常
xxxxxxxxxx
Stream<String> stream =Stream.of("aa","bb","cc");
//以下两步第二步就会报错,提示流已经关闭或者已经操作过
long count=stream.count();
long count2=stream.count();
Stream流的非终结方法返回的流是新的流,两个流不是同一个对象
xxxxxxxxxx
//Stream方法返回的是新的流
public void testStreamAPI(){
Stream<String> stream = Stream.of("aa", "bb", "cc");
Stream<String> limit = stream.limit(1);
System.out.println("stream"+stream);//streamjava.util.stream.ReferencePipeline$Head@25618e91
System.out.println("limit" + limit);//limitjava.util.stream.SliceOps$1@7a92922
}
Stream不调用终结方法,中间的非终结操作是不会执行的
xxxxxxxxxx
//Stream方法不调用终结方法,中间的方法操作是不会执行的
public void testStreamAPI2(){
Stream<String> stream = Stream.of("aa", "bb", "cc");
//不写终结方法,非终结方法是不会执行的
//stream.filter((str)->{
// System.out.println(str);
// return true;
//});
//写了终结方法,非终结方法才会执行
stream.filter((str)->{
System.out.print(str);//aabbcc
return true;
}).count();
}
在流操作完成后,可以将Stream流的元素放到集合或者数组中,可以收集流中的数据
将流中的数据收集到集合中
实例
xxxxxxxxxx
//将流中的数据收集到集合中
public void testCollect(){
Stream<String> stream = Stream.of("aa", "bb", "cc", "bb");
//1.将流中的数据收集到list集合中,
//List<String> list = stream.collect(Collectors.toList());
//System.out.println("list:"+list);
//2.将流中的数据收集到set集合中
//Set<String> set = stream.collect(Collectors.toSet());
//System.out.println("set:"+set);
//3. 将流中的数据收集到ArrayList集合中
ArrayList<String> arrayList = stream.collect(Collectors.toCollection(ArrayList::new));
System.out.println("arrayList:"+arrayList);
//4. 将流中的数据搜集到,set集合和HashSet集合会自动去掉重复的数据
//HashSet<String> hashSet = stream.collect(Collectors.toCollection(HashSet::new));
//System.out.println("HashSet:"+hashSet);
}
//5. 将流中的数据搜集到Map集合中
public Map<Long, String> getIdNameMap(List<Account> accounts) {
return
accounts.stream().collect(Collectors.toMap(Account::getId, Account::getUsername));
}
将流中的数据搜集到数组中
实例
xxxxxxxxxx
//将流中的数据搜集到数组中
public void testStreamToArray(){
Stream<String> stream = Stream.of("aa", "bb", "cc", "bb");
//1.将流中的数据转成Object数组,Object数组不是很方便,比如想使用String的length方法还需要向下强转为String才能执行相关操作
//Object[] objects = stream.toArray();
//for (Object o:objects) {
// System.out.print("Objects:"+o+",");
//}
//2.将流中的数据转成指定类型的数组
String[] strings = stream.toArray(String[]::new);
for (String str:strings) {
System.out.print("String:"+str+"|"+str.length()+",");
}
}
聚合计算和数据库的对字段操作是一样的,如获取最大值、最小值、求总和、平均值、统计数量
获取最大值调用max方法也能单独实现,这里面的操作基本直接调用Stream本身的API就能实现
API
实例
xxxxxxxxxx
public void testAggregateFunction(){
Stream<Student> studentStream = Stream.of(
new Student("赵丽颖", 58, 95),
new Student("杨颖", 56, 88),
new Student("迪丽热巴", 56, 99),
new Student("柳岩", 52, 77)
);
//1. 获取某个属性最大值的对应元素,max函数返回的也是元素
//max函数
//Optional<Student> max = studentStream.max((student1, student2) -> student1.getScore() - student2.getScore());
//System.out.println("最高成绩学生:"+max.get());
//聚合函数Collectors.maxBy(),竟然不能和max函数合用,貌似一个流不能被多次操作,先后maxBy和minBy都会出错
//Optional<Student> maxScoreStudent = studentStream.collect(Collectors.maxBy((student1, student2) -> student1.getScore() - student2.getScore()));
//System.out.println("最高成绩学生:"+maxScoreStudent.get());
//2.获取某个属性最小值的对应元素
//Optional<Student> minScoreStudent = studentStream.collect(Collectors.minBy((student1, student2) -> student1.getScore() - student2.getScore()));
//System.out.println("最低成绩学生:"+minScoreStudent.get());
//3.将元素的属性求和
//求和是summingInt函数实现的,该函数的传参是函数式接口ToIntFunction的匿名内部类,作用是获取要求和的属性值并将其转换为int基本类型
//Integer sum = studentStream.collect(Collectors.summingInt(s -> s.getAge()));
//System.out.println("学员年龄总和:"+sum);
//4,将元素的属性值求平均值,
//averagingInt的传参依然是一个函数式接口ToIntFunction的匿名内部类,注意传参int类型,返回值是Double类型
//Double averageScore = studentStream.collect(Collectors.averagingInt(s -> s.getScore()));
//自定义类也可以使用方法引用,这里是通过类名引用实例方法
//Double averageScore = studentStream.collect(Collectors.averagingInt(Student::getScore));
//System.out.println("学员平均成绩:"+averageScore);
//5. 统计流中元素的数量 ,用count方法也能统计流中元素的数量
Long count = studentStream.collect(Collectors.counting());
System.out.println("流中元素的数量:"+count);
}
分组API
对流中的数据根据某个标准进行一级分组
xxxxxxxxxx
public void testGroup(){
Stream<Student> studentStream = Stream.of(
new Student("赵丽颖", 52, 95),
new Student("杨颖", 56, 88),
new Student("迪丽热巴", 56, 40),
new Student("柳岩", 52, 50)
);
//groupingBy需要的参数是函数式接口Function[传入一个参数经过加工返回一个参数],参数是分类的依据
//这个地方很奇妙,返回值即分组的依据不一定是元素的某个属性,感觉更像对每个元素进行进行处理的过程中调用函数式接口的实现类并将元素
//归类为对应的函数式接口Function的apply函数返回值
Map<Integer, List<Student>> groupByAge = studentStream.collect(Collectors.groupingBy((student) -> student.getAge()));
System.out.println(groupByAge);//{52=[Student{name='赵丽颖', age=52, score=95}, Student{name='柳岩', age=52, score=50}], 56=[Student{name='杨颖', age=56, score=88}, Student{name='迪丽热巴', age=56, score=40}]}
//Map<String, List<Student>> groupByScore = studentStream.collect(Collectors.groupingBy(student -> student.getScore() >= 60 ? "及格" : "不及格"));
//返回的是一个Map,key是分类的依据;value是对应元素的List集合
//System.out.println(groupByScore);//{不及格=[Student{name='迪丽热巴', age=56, score=40}, Student{name='柳岩', age=52, score=50}], 及格=[Student{name='赵丽颖', age=52, score=95}, Student{name='杨颖', age=56, score=88}]}
}
对流中的数据进行多级分组
逻辑是先针对某个标准进行分组,再在同一个分组中根据另外一个标准再次进行分组;使用的是
groupingBy
的重载方法,参数列表是Function接口和Collector接口的groupingBy
单参方法的返回值【同样需要指定Function接口的匿名实现,即返回第二级分组标准】;Function是第一级分类的标准,Collector是第二级分类的标准返回的仍然是一个map,map的key是第一层的分类标准;map的value仍然是一个Map,是二级分类
xxxxxxxxxx
public void testMultiLevelGroup(){
Stream<Student> studentStream = Stream.of(
new Student("赵丽颖", 52, 95),
new Student("杨颖", 56, 88),
new Student("迪丽热巴", 56, 40),
new Student("柳岩", 52, 50)
);
/**public static <T, K, A, D>
Collector<T, ?, Map<K, D>> groupingBy(Function<? super T, ? extends K> classifier,
Collector<? super T, A, D> downstream) {
return groupingBy(classifier, HashMap::new, downstream);
}
第一个参数是Function接口指定一级分类的标准,第二个参数是一个Collector,一般是Collectors.groupingBy()的单参方法
并重写Function接口即指定二级分类标准
*/
Map<Integer, Map<String, List<Student>>> studentGroupMultiLevel = studentStream.collect(Collectors.groupingBy(student -> student.getAge(), Collectors.groupingBy(student -> student.getScore() >= 60 ? "及格" : "不及格")));
//Map是重写了toString方法的,可以直接打印
System.out.println(studentGroupMultiLevel);
/**
* {52={
* 不及格=[Student{name='柳岩', age=52, score=50}],
* 及格=[Student{name='赵丽颖', age=52, score=95}]
* },
* 56={
* 不及格=[Student{name='迪丽热巴', age=56, score=40}],
* 及格=[Student{name='杨颖', age=56, score=88}]
* }
* }
* */
}
数据分区和数据分组类似,但是数据分区只能分成
true
和false
两个组,数据分组可以分成多个组且还能多级分组
API
实例
xxxxxxxxxx
//测试数据分区
public void testPartition(){
Stream<Student> studentStream = Stream.of(
new Student("赵丽颖", 52, 95),
new Student("杨颖", 56, 88),
new Student("迪丽热巴", 56, 40),
new Student("柳岩", 52, 50)
);
//partitioningBy的参数类型是函数式接口Predicate,意味着分区key只能是true和false
Map<Boolean, List<Student>> scorePartition = studentStream.collect(Collectors.partitioningBy(student -> student.getScore() >= 60));
System.out.println(scorePartition);
/**
*
{
false=[Student{name='迪丽热巴', age=56, score=40}, Student{name='柳岩', age=52, score=50}],
true=[Student{name='赵丽颖', age=52, score=95}, Student{name='杨颖', age=56, score=88}]
}
*/
}
将String类型的数据根据指定规则进行字符串拼接
数据间拼接一个指定间隔字符串
实例
xxxxxxxxxx
//测试数据拼接
public void testJoining(){
Stream<Student> studentStream = Stream.of(
new Student("赵丽颖", 52, 95),
new Student("杨颖", 56, 88),
new Student("迪丽热巴", 56, 40),
new Student("柳岩", 52, 50)
);
//1. 数据之间拼接一个间隔字符串,返回一个字符串
//joining(CharSequence delimiter)
//String studentName = studentStream.map(Student::getName).collect(Collectors.joining(" | "));
//System.out.println(studentName);//赵丽颖 | 杨颖 | 迪丽热巴 | 柳岩
//2. 数据之间拼接一个间隔字符串,首尾加前缀后缀
String studentName = studentStream.map(Student::getName).collect(Collectors.joining(" | ", "前缀", "后缀"));
System.out.println(studentName);//前缀赵丽颖 | 杨颖 | 迪丽热巴 | 柳岩后缀
}
上述的Stream流都是串行Stream流,效率低
串行Stream流
对每个元素的处理都使用的同一个线程
xxxxxxxxxx
//串行Stream流
public void testStreamThread(){
Stream.of(4,5,3,9,1,2,6).filter(data->{
System.out.println(Thread.currentThread()+" | "+data+","+data.getClass());
/**
* Thread[main,5,main] | 4,class java.lang.Integer
* Thread[main,5,main] | 5,class java.lang.Integer
* Thread[main,5,main] | 3,class java.lang.Integer
* Thread[main,5,main] | 9,class java.lang.Integer
* Thread[main,5,main] | 1,class java.lang.Integer
* Thread[main,5,main] | 2,class java.lang.Integer
* Thread[main,5,main] | 6,class java.lang.Integer
* 串行Stream流对每个元素的处理都使用的同一个线程
* */
return data>3;
}).count();
}
获取并行流的两种方式
方式一:直接通过list
集合中的实例方法parallelStream
方法获取并行流,list.parallelStream()
方式二:通过串行stream流的实例方法stream.parallelStream()
获取对应的并行流
实例
xxxxxxxxxx
//获取并行Stream流
public void testParallelStream(){
//1. 方式一:直接通过list集合的实例方法获取并行Stream流
ArrayList<String> list = new ArrayList<>();
//并行stream流应该也是Stream的子实现类
Stream<String> stream = list.parallelStream();
//2. 方式二:将串行流转成并行流,串行流中的parallel()方法返回的就是并行流
Stream<String> parallel = list.stream().parallel();
}
并行Stream流
并行流的不同元素处理使用的是不同的线程
xxxxxxxxxx
//并行Stream流
public void testStreamThreadParallel(){
Stream.of(4,5,3,9,1,2,6).parallel().filter(data->{
System.out.println(Thread.currentThread()+" | "+data+","+data.getClass());
/**
* Thread[main,5,main] | 1,class java.lang.Integer
* Thread[ForkJoinPool.commonPool-worker-1,5,main] | 5,class java.lang.Integer
* Thread[ForkJoinPool.commonPool-worker-6,5,main] | 3,class java.lang.Integer
* Thread[ForkJoinPool.commonPool-worker-5,5,main] | 2,class java.lang.Integer
* Thread[ForkJoinPool.commonPool-worker-4,5,main] | 9,class java.lang.Integer
* Thread[ForkJoinPool.commonPool-worker-3,5,main] | 6,class java.lang.Integer
* Thread[ForkJoinPool.commonPool-worker-2,5,main] | 4,class java.lang.Integer
* 并行Stream流对每个元素的处理都使用的不同的线程
* */
return data>3;
}).count();
}
并行stream
流的效率
xxxxxxxxxx
//作为测试的开始时间
long start;
//测试stream和并行stream的效率,设置循环次数相加操作消耗的时间,设置循环相加5亿次
long times=500000000;
//@Before注解是junit自带的注解,会在测试程序运行前先执行
public void init(){
start=System.currentTimeMillis();
}
//@After也是junit自带的注解,会在测试程序运行后执行
public void after(){
long end=System.currentTimeMillis();
System.out.println("消耗时间:"+(end-start)+"ms");
}
//单纯的5亿次相加运算 消耗时间:162ms
public void testEfficiencyLocal(){
int sum=0;
for (int i = 0; i < times; i++) {
sum+=i;
}
}
//串行stream流的5亿次相加操作 消耗时间:771ms
public void testEfficiencyStream(){
//LongStream.rangeClosed(0,times)是得到0到指定值之间的整数Long类型的流,这里的流有五亿个元素
LongStream.rangeClosed(0,times).reduce(0,Long::sum);
}
//并行stream流的5亿次相加操作 消耗时间:50ms
//并行的stream流会采用分治算法将大任务切分成多个小任务,每个小任务分配到独立的线程上运行,并行Stream流的效率是最高的,且远超串行stream流
public void testEfficiencyParallelStream(){
LongStream.rangeClosed(0,times).parallel().reduce(0,Long::sum);
}
多线程操作可能存在线程安全问题
一般的解决方法有给涉及线程安全的操作加锁放在同步代码块中,使用线程安全的集合,或者调用Stream接口的toArray或者collect方法【返回集合或者数组】对数据的操作就是线程安全的
xxxxxxxxxx
//并行stream的线程安全问题,多线程操作下对不安全的ArrayList集合执行添加或者修改操作,可能由于两个线程同时对集合操作,导致集合数据丢失
public void testParallelStreamSecurity(){
ArrayList<Integer> list = new ArrayList<>();
//IntStream.rangeClosed(1,1000).parallel().forEach(i->list.add(i));
//System.out.println("list集合的容量大小:"+list.size());//list集合的容量大小:980 7ms
//1. 线程安全方案1:使用同步代码块用类锁锁代码执行
//Object obj = new Object();
//IntStream.rangeClosed(1,1000).parallel().forEach(i->{
// synchronized (obj){
// list.add(i);
// }
//});
//System.out.println("list集合的容量大小:"+list.size());//list集合的容量大小:1000 消耗时间:7ms
//2. 线程安全方案2:ArrayList是线程不安全的,使用Vector线程安全的集合,
//Vector<Integer> vector = new Vector();
//IntStream.rangeClosed(1,1000).parallel().forEach(i->vector.add(i));
//System.out.println("vector集合的容量大小:"+vector.size());//vector集合的容量大小:1000 消耗时间:8ms
//也可以使用api:Collections.synchronizedList(list)把线程不安全的集合变成线程安全的集合
//List<Integer> synchronizedList = Collections.synchronizedList(list);
//IntStream.rangeClosed(1,1000).parallel().forEach(i->synchronizedList.add(i));
//System.out.println("synchronizedList:"+synchronizedList.size());//synchronizedList:1000 消耗时间:8ms
//3. 线程安全方案3,调用Stream流的collect/toArray方法收集流中的数据到list集合中也是线程安全的
//IntStream调用collect方法传参比较复杂,先执行boxed方法就可以获取Integer类型的Stream流,此时调用collect方法只需要按照固定写法
List<Integer> collect = IntStream.rangeClosed(1, 1000).parallel().boxed().collect(Collectors.toList());
System.out.println("collect:"+collect.size());
}
并行Stream流的实现原理
parallelStream
是基于Fork/Join
框架实现的
parallelStream
底层使用的不是普通的线程,使用的是Fork/Join
框架;Fork/Join
是JDK7引入的新的线程框架,作用是可以将一个大任务拆分成很多小任务来异步执行
Fork/Join
框架主要包含三个模块
线程池:ForkJoinPool
,继承自Executor
,是线程池
任务对象:ForkJoinTask
,表示拆分出来的小任务,实现了Future
和Serializable
接口,实际使用是自定义任务类继承ForkJoinTask
的子类
执行任务的线程对象:ForkJoinWorkerThread
,继承于Thread
Fork/Join
框架的原理
Fork/Join
框架使用分治算法来将大任务拆分成小任务,比如排序将1000w个数分为两个500w个数的排序任务,再对500w个数的排序任务再分割,直到最后的排序任务个数达到一定的阈值才会使用其他排序方法局部排序,排序完成以后对局部有序的结果再合并,Fork是大任务拆分成小任务的过程,Join是小任务合并为大任务的过程
工作窃取算法
Fork/Join
最核心的地方是利用现代硬件设备多核,利用空闲的CPU提高性能,工作窃取算法的作用是某个线程从其他队列中窃取任务来执行,某个线程把任务执行完空闲以后去别的线程窃取排队最后的任务来执行
Fork/Join
框架实例
一个计算1-10000的任务,先对任务进行拆分,加法运算的数量大于3000就拆分任务,小于3000就进行运算
在计算次数较少的情况下,比如1000w次相加,仍然是直接用单线程循环遍历相加会快一些,任务管理器--CPU--图形右键--将图形更改为--逻辑处理器,可以查看CPU的线程占用率;Fork/Join执行任务会所有的CPU线程都打满,而单线程遍历运算只会拉起一个CPU线程的占用率
代码实现
【求和任务类】
xxxxxxxxxx
class SumRecursiveTask extends RecursiveTask<Long>{
//定义计算任务不再拆分的阈值
private static final long THRESHOLD=3000L;
//定义计算任务的初始值
private final long start;
//定义计算任务的结束值
private final long end;
public SumRecursiveTask(long start,long end){
this.start=start;
this.end=end;
}
protected Long compute() {
//两个数一次计算
long length=end-start;
if(length<=THRESHOLD){
//任务不用拆分可以直接计算
long sum=0;
for (long i = start; i <=end ; i++) {
sum+=i;
}
//System.out.println("计算:"+start+"->"+end+"结果为:"+sum);
return sum;
}else{
//数量大于拆分的阈值,任务继续拆分
long middle=(start+end)/2;
//System.out.println("任务拆分:左"+start+"->"+middle+",右:"+(middle+1)+"->"+end);
SumRecursiveTask leftTask = new SumRecursiveTask(start, middle);
leftTask.fork();
SumRecursiveTask rightTask = new SumRecursiveTask(middle + 1, end);
rightTask.fork();
return leftTask.join()+rightTask.join();
}
}
}
【代码执行效果】
xxxxxxxxxx
public class TestStream{
//测试ForkJoin框架实现任务拆分和多线程并行执行各个任务
/**
* 执行效果:
* 任务拆分:左1->5000,右:5001->10000
* 任务拆分:左1->2500,右:2501->5000
* 任务拆分:左5001->7500,右:7501->10000
* 计算:2501->5000结果为:9376250
* 计算:5001->7500结果为:15626250
* 计算:7501->10000结果为:21876250
* 计算:1->2500结果为:3126250
* 1->10000两两相加的结果为:50005000
* */
public void testForkJoin(){
//创建线程池对象
ForkJoinPool forkJoinPool = new ForkJoinPool();
//创建能拆分计算任务的任务类
SumRecursiveTask sumRecursiveTask = new SumRecursiveTask(1, 10000000L);
//线程池对象通过invoke方法执行任务类
Long result = forkJoinPool.invoke(sumRecursiveTask);
//System.out.println("1->10000两两相加的结果为:"+result);
System.out.println(result);
}
//这个不拆分竟然比拆分多线程计算快,而且数据量越大快的越多,离谱;1000w的计算量直接单线程循环比Fork/Join快,而且快的多,前者只需要3ms,后者需要116毫秒
//后来相加运算设置成100000000000L,不拆分单线程需要二十多秒,但是拆分多线程只需要8秒
public void test(){
long sum=0;
for (long i = 1; i <= 10000000L; i++) {
sum+=i;
}
System.out.println(sum);
}
}
小结
并行stream流parallelStream是线程不安全的,适用于CPU密集型场景,主要目的还是不要浪费CPU的性能,如果CPU本身就负载很大了还使用并行流,并不能起到很明显的效果,还可能影响其他程序的执行
一般并行Stream流不适用于I/O密集型操作【磁盘I/O、网络I/O都属于I/O操作】,I/O操作一般不依赖CPU,使用并行流进行大批量的消息推送涉及大量I/O,反而会导致使用并行流更慢
并行流使用时无法保证元素的顺序,即使用集合同步流数据即使保证了元素都正确也无法保证不同线程将指定元素按照顺序放在一个集合中
增强点
JDK8以前的接口只能放静态常量和抽象方法,JDK8对接口进行增强,还可以放默认方法和静态方法
JDK8接口
xxxxxxxxxx
interface 接口名{
静态常量;
抽象方法;
默认方法;
静态方法;
}
增强原因
默认方法
接口中只有抽象方法,接口新增抽象方法,所有的实现类都必须重写该抽象方法,不利于接口的扩展
接口中的默认方法实现类不需要重写就可以直接进行使用,实现类也可以根据需要进行重写
方便接口的扩展
默认方法在实现类中重写不需要default关键字
默认方法语法格式
默认方法有方法体,与类中一般方法的定义相比只是多了一个default关键字
xxxxxxxxxx
interface 接口名{
修饰符 default 返回值类型 方法名(){
方法体;
}
}
静态方法
静态方法不能被实现类直接调用,也不能被实现类重写,只能通过接口名.方法名()
进行调用
静态方法语法格式
与默认方法相比只是关键字default改成了static
xxxxxxxxxx
interface 接口名{
修饰符 static 返回值类型 方法名(){
方法体;
}
}
默认方法和静态方法的区别
默认方法必须通过实例调用,静态方法通过接口名调用
默认方法可以被实现类继承,实现类可以直接使用接口定义的默认方法,也可以重写接口中的默认方法;静态方法不能被继承也不能被实现类重写,只能通过接口名进行调用
接口中的方法需要被实现类继承或重写,使用默认方法;不需要被继承或者重写就直接使用静态方法
Optional
类只有两种状态,没有子类,一般看成一个容器,要么Optional
中有值,要么就是null
;主要作用是避免空指针检查,防止NullPointerException
,结合方法orElse
,ifPresent
,ifPresentOrElse
,map
能够写出优雅的判空处理代码
传统对指针判空的处理方式
实例
xxxxxxxxxx
//传统对指定判空处理的方式
public void testTraditionalHandleNull(){
String userName="zhangSan";
//对引用判空进行处理
if(userName!=null){
System.out.println("用户姓名:"+userName);
}else{
System.out.println("姓名不存在");
}
}
Optional
类的API
Optional<T> --> Optional.of(T t)
作用:创建一个Optional
对象,of
方法只能传入具体值,不能传入null
,传入null会报空指针异常
Optional<T> --> Optional.ofNullable(T t)
作用:创建一个Optional
对象,ofNullable
方法既能传入具体值,也能传入null
Optional<T> --> Optional.empty()
作用:创建一个内容为null
的Optional
对象
boolean --> optional.isPresent()
作用:判断optional
中是否有具体值,有具体值就返回true,如果没有值或者值为null就返回false
T t-->optional.get()
作用:获取optional
中的具体值,如果optional有具体值就返回具体值,没有值或者为null就会抛没有这个元素的异常;所以一般操作都使用optional.isPresent()
对是否有值进行判断再决定是否取值进行处理
Optional --> optional.map(Function<T> function)
作用://TODO,课上没讲但是在对用户名字转大写案例中使用了,很好用,功能和Stream流的map方法类似,看文档研究研究
使用Optional类对可能存在空值的情况处理
使用apioptional.isPresent()
来判断是否空值进一步处理
xxxxxxxxxx
//使用Optional对可能存在空值的情况的处理
public void testHandleNull(){
Optional<Object> op1 = Optional.empty();
Optional<String> op2 = Optional.ofNullable("张三");
if(op2.isPresent()){
System.out.println(op2.get());
}else{
System.out.println("没有值");
}
}
T --> optional.orElse(T t)
作用:如果optional
中有值就返回该具体值,如果optional
中没有值就使用传参作为返回值
实例:
xxxxxxxxxx
//测试orElse方法使用默认值
public void testOrElse(){
Optional<String> userName = Optional.ofNullable("张三");//张三
//Optional<String> userName = Optional.ofNullable(null);//没有值
String user = userName.orElse("没有值");
System.out.println(user);
}
void --> optional.ifPresent(Comsumer<T> comsumer)
作用:如果optional
中有具体值就执行函数式接口Consumer的匿名实现,ifPresent
方法自动传参optional中的具体值,如果optional
中没有值就会什么也不做
实例:
xxxxxxxxxx
//测试IfPresent方法使用接口对判空进行处理
//optional有值就执行函数式接口Consumer的实现,没有值就啥都不做
public void testIfPresent(){
Optional<String> userName = Optional.ofNullable("张三");//有值,值为:张三
//Optional<Object> userName = Optional.ofNullable(null);
//public void ifPresent(Consumer<? super T> action) { 传参Comsumer函数式接口
//ifPresent的意思是如果有值就调用Consumer接口使用参数,会自动传参optional的具体值;没有值会啥事都不发生
userName.ifPresent(user->{
System.out.println("有值,值为:"+user);
});
}
void --> optional.ifPresentOrElse(Consumer<T> comsumer,Runnable runnable)
作用:optional对象中存在具体值调用Consumer接口,没有值就执行Runnable接口的匿名实现
实例:
xxxxxxxxxx
//测试ifPresentOrElse方法使用接口对判空后进行处理
public void testIfPresentOrElse(){
//Optional<String> userName = Optional.ofNullable("张三");//有值,值为:张三
Optional<String> userName = Optional.ofNullable(null);//没有值
//public void ifPresentOrElse(Consumer<? super T> action, Runnable emptyAction) {
//Consumer接口是判定optional有值以后对值处理的接口,Runnable接口是判定optional没有值以后的处理接口,Runnable接口就是一个空参的run方法
userName.ifPresentOrElse(user-> System.out.println("有值,值为:"+user),
()-> System.out.println("没有值"));
}
需求实现:将用户的名字属性全部转换成大写
java中的toUpperCase会直接将中文原样输出
xxxxxxxxxx
//用optional优雅实现对用户的某个属性判空和后续操作,如果名字属性不为空就统一转换大写
public void testOptionalCase(){
//Person user = new Person(null, 26);//null
Person user = new Person("Mike", 26);//MIKE
//将user对象封装成Optional对象
Optional<Person> optionalData = Optional.of(user);
//使用传统判空方式实现
String upperUserNameTraditional = getUpperUserNameTraditional(user);
System.out.println(upperUserNameTraditional);
//使用Optional方式实现
String upperUserNameOptional = getUpperUserNameOptional(optionalData);
System.out.println(upperUserNameOptional);
}
//使用Optional对判空进行处理
public String getUpperUserNameOptional(Optional<Person> optional){
//optional也有map方法,作用似乎和stream流中的map方法一致,这个是链式处理完以后判断最终结果是否为空,不是原样输出,是就返回字符串null
String userUpperName = optional.map(person -> person.getName()).
map(userName -> userName.toUpperCase()).
orElse("null");
return userUpperName;
}
//传统方式对判空进行处理,首先要保证person不为空,然后保证person中的name属性不为空
public String getUpperUserNameTraditional(Person person){
if(person!=null){
String userName = person.getName();
if (userName!=null) {
return userName.toUpperCase();
}else{
return "null";
}
}else{
return "null";
}
}
Java8中新增了日期和时间的API
新版本中对日期时间的API的操作会生成全新的时间日期对象实例,不会影响老值;新版本给人提供的
LocalDate
、LocalTime
、...、ZoneDate
来给人使用,提供Instant
来给程序使用;通过时间调整器TemporalAdjuster
可以更快更精确地调整时间日期;此外还是时间解析线程安全的新的时间日期类中,
LcoalDate
表示日期【年月日】,LcoalTime
表示时间【时分秒】,LocalDateTime
包含日期和时间【年月日时分秒】,DateTimeFormatter
对时间进行格式化和解析,主要供程序使用方便对秒和纳秒操作的Instant
类,计算日期时间间隔的Duration/Period
,方便调整时间的TemporalAdjuster
,以及带时区的日期、时间、日期时间,分别为ZoneDate
、ZoneTime
、ZoneDateTime
旧版日期时间API的问题
设计不合理,Date
类有两个,一个是util
包下的,一个sql
包下的
对日期时间解析非线程安全
时区处理麻烦,日期类不提供国际化时区支持
实例
xxxxxxxxxx
public void testDate(){
//1. 设计不合理,有两个Date,且年会加上1900
Date date = new Date(1985, 9, 23);
System.out.println(date);//Fri Oct 23 00:00:00 CST 3885
//传1985年会显示3885年,源码提示是加了1900年,1900+1985=3885
//2.时间格式化和解析是线程不安全的
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
//以下多线程同时解析"2019-09-09"会抛异常且出现很多解析时间对不上"2019-09-09"的情况
for (int i = 0; i < 50; i++) {
new Thread(()->{
try{
Date date1 = sdf.parse("2019-09-09");
System.out.println("date="+date1);
}catch (ParseException e){
e.printStackTrace();
}
}).start();
}
//3. 旧版API处理时区也很麻烦,日期类不提供国际化,没有时区支持,java引入java.util.Calendar和java.util.TimeZone来解决使其问题
}
新增时间日期API
线程安全、API设计合理,全部位于
java.time
包下,其中的一些关键类Java使用的历法是ISO 8601日历系统【世界民用历法,即公历,平年365天,闰年366天】,Java8还支持其他4套历法,分别为
ThaiBuddhistDate
:泰国佛教历,MinguoDate
:中华民国历,JapaneseDate
:日本历,HijrahDate
:伊斯兰历
LocalDate
:日期,包含年月日,格式为2019-10-16
实例
xxxxxxxxxx
/**
* 测试LocalDate
* now:2024-02-21
* date:2024-02-21
* 年:2024
* 月:FEBRUARY
* 月:2
* 日:52
* 日:21
* 日:WEDNESDAY
* */
public void testLocalDate(){
//1. 获取当前日期,年-月-日并返回LocalDate对象
LocalDate now = LocalDate.now();
System.out.println("now:"+now);
//2. 根据指定年月日获取LocalDate对象
LocalDate date = LocalDate.of(2024, 2, 21);
System.out.println("date:"+date);
//3. 分别获取LocalDate的年月日信息,日可以获取一个月的哪一天,
System.out.println("年:"+date.getYear());
//月和周几都显示的是英文全大写,因为返回值的类型是枚举类型,要返回数字可以使用localDate.getMonthValue()
//周几似乎没有转成数字的api
System.out.println("月:"+date.getMonth());
//返回数字类型的月value
System.out.println("月:"+date.getMonthValue());
System.out.println("日:"+date.getDayOfYear());
System.out.println("日:"+date.getDayOfMonth());
System.out.println("日:"+date.getDayOfWeek());
}
LocalTime
:表示时间,包含时分秒,格式为16:38:54.158549300
实例:
xxxxxxxxxx
/**
* 测试LocalTime
* now:09:02:14.775777900
* time:08:59
* 时:9
* 分:2
* 秒:14
* 纳秒:775777900
* */
public void testLocalTime(){
//1. 获取当前时间的时分秒
LocalTime now = LocalTime.now();
System.out.println("now:"+now);
//2. 获取指定时间的时分秒对应的LocalTime对象
LocalTime time = LocalTime.of(8, 59, 0);
System.out.println("time:"+time);
//3. 获取LocalTime对象的时分秒纳秒信息,竟然没看到毫秒
System.out.println("时:"+now.getHour());
System.out.println("分:"+now.getMinute());
System.out.println("秒:"+now.getSecond());
System.out.println("纳秒:"+now.getNano());
}
LocalDateTime
:表示日期时间,包含年月日时分秒,格式为2018-09-06T15:33:56.750
实例:
xxxxxxxxxx
/**
* 测试LocalDateTime
* now:2024-02-21T09:23:24.943226100
* time:2024-02-21T09:12:30
* 年:2024
* 月:FEBRUARY
* 月:2
* 年日:52
* 月日:21
* 周几:WEDNESDAY
* 时:9
* 分:23
* 秒:24
* 纳秒:943226100
* */
public void testLocalDateTime(){
//1. 获取当前时间的年月日时分秒的LocalDateTime对象
LocalDateTime now = LocalDateTime.now();
System.out.println("now:"+now);
//2. 获取指定时间的年月日时分秒对应的LocalDateTime对象
LocalDateTime time = LocalDateTime.of(2024, 2, 21, 9, 12, 30);
System.out.println("time:"+time);
//3. 获取LocalDateTime对象的年月日秒
System.out.println("年:"+now.getYear());
System.out.println("月:"+now.getMonth());
System.out.println("月:"+now.getMonthValue());
System.out.println("年日:"+now.getDayOfYear());
System.out.println("月日:"+now.getDayOfMonth());
System.out.println("周几:"+now.getDayOfWeek());
System.out.println("时:"+now.getHour());
System.out.println("分:"+now.getMinute());
System.out.println("秒:"+now.getSecond());
System.out.println("纳秒:"+now.getNano());
}
【修改LocalDateTime】
修改其他的
LocalDate
,LocalTime
也是一样的改法;修改对应的LcoalDateTime
会生成一个修改后全新的LocalDateTime
对象,不会影响原来的对象
xxxxxxxxxx
/**
* 测试修改时间
* nowModify:2025-02-21T09:53:45.626901900
* now==nowModify?false
* localDateTime:2026-02-21T09:53:45.626901900
* now==localDateTime?false
* 2014-02-21T09:53:45.626901900
* */
public void testModifyLocalDateTime() {
//1. 通过withXxx来修改时间,返回新的时间对象
LocalDateTime now = LocalDateTime.now();
LocalDateTime nowModify = now.withYear(2025);
System.out.println("nowModify:"+nowModify);
System.out.println("now==nowModify?"+(now==nowModify));
//2. 通过plusXxx来增加时间
//增加两年,返回的也是全新的时间对象
LocalDateTime localDateTime = now.plusYears(2);
System.out.println("localDateTime:"+localDateTime);
System.out.println("now==localDateTime?"+(now==localDateTime));
//3. 通过minusXxx来减去时间
//减少十年
System.out.println(now.minusYears(10));
}
【比较时间】
xxxxxxxxxx
/**
* 测试比较时间
* true
* false
* false
* */
public void testCompareLocalDateTime(){
LocalDateTime time = LocalDateTime.of(2023, 7, 12, 13, 28, 30);
LocalDateTime now = LocalDateTime.now();
//localDateTime1.isAfter(localDateTime2)是比较localDateTime1晚于localDateTime2
System.out.println(now.isAfter(time));//true
//前一个时间是否早于后一个时间
System.out.println(now.isBefore(time));//false
//前一个时间是否等于后一个时间
System.out.println(now.isEqual(time));//false
}
DateTimeFormatter
:用于对时间日期进行格式化解析
实例:
xxxxxxxxxx
/**
* 测试时间格式
* */
public void testDateTimeFormatter(){
LocalDateTime now = LocalDateTime.now();
//1. 使用JDK自带的一些时间格式,返回对应格式的格式化器dtf,除了ISO_DATE_TIME还有非常多的时间格式
DateTimeFormatter dtf = DateTimeFormatter.ISO_DATE_TIME;
String nowFormat1 = now.format(dtf);
System.out.println(nowFormat1);//2024-02-21T10:31:19.4412392
//2. 使用DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH时mm分ss秒")自定义日期格式
DateTimeFormatter dtf2 = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH时mm分ss秒");
String nowFormat2 = now.format(dtf2);
System.out.println(nowFormat2);//2024年02月21日 10时35分52秒
//3. 根据时间字符串和对应的格式DateTimeFormatter对象解析对应的时间字符串成LocalDateTime对象
//LocalDateTime time = LocalDateTime.parse("2020年09月20日 15时15分15秒", dtf2);
//System.out.println(time);//2020-09-20T15:15:15
//同时解析也是线程安全的,解析过程不像此前第一个方法的多线程时间解析一堆异常和时间解析错误
for (int i = 0; i < 50; i++) {
new Thread(()->{
LocalDateTime time = LocalDateTime.parse("2020年09月20日 15时15分15秒", dtf2);
System.out.println(time);//2020-09-20T15:15:15
}).start();
}
}
ZoneDateTime
:包含对时区解析的时间
Instant
:时间戳,一个当前瞬时时间
实例:
xxxxxxxxxx
/**
* 对时间戳进行操作
* */
public void testInstant(){
//Instant内部保存了秒和纳秒,一般不是给用户使用的,主要目的是方便程序做统计的,比如统计程序的执行时间
//1. 创建一个时间戳
Instant now = Instant.now();
System.out.println("now="+now);//now=2024-02-21T03:06:53.496814Z
//Instant修改时间不能修改年月日,只能修改秒、毫秒、纳秒
//2. 给时间戳加上一段时间
Instant delayNow = now.plusSeconds(10);
System.out.println("delayNow:"+delayNow);//delayNow:2024-02-21T03:10:26.456514400Z
//3. 给时间戳减去一段时间
Instant preNow = now.minusSeconds(20);
System.out.println("preNow:"+preNow);//preNow:2024-02-21T03:14:53.361621500Z
//4. 获取时间戳的纳秒、秒信息
//获取时间戳的秒信息
System.out.println(now.getEpochSecond());//1708485437
//获取时间戳的纳秒信息
System.out.println(now.getNano());//174471100
}
Duration
:用于计算两个LocalTime
时间间的时长间隔
实例:
xxxxxxxxxx
/**
* 测试时间间隔Duration类的API
* */
public void testDuration(){
//Duration计算时间间隔
LocalTime now = LocalTime.now();
LocalTime time = LocalTime.of(14, 14, 14);
Duration duration1 = Duration.between(time, now);
//通过时间间隔的API将时间间隔转成天数、小时数、分钟数和秒数。注意如果第一个参数比后一个参数晚会显示对应的负数
//早会显示正数
System.out.println("相差的天数:"+duration1.toDays());//相差的天数:0
System.out.println("相差的小时数:"+duration1.toHours());//相差的小时数:-2
System.out.println("相差的分钟数:"+duration1.toMillis());//相差的分钟数:-9507161
System.out.println("相差的秒数:"+duration1.toSeconds());//相差的秒数:-9508
Duration duration2 = Duration.between(now, time);
System.out.println("相差的天数:"+duration2.toDays());//相差的天数:0
System.out.println("相差的小时数:"+duration2.toHours());//相差的小时数:2
System.out.println("相差的分钟数:"+duration2.toMillis());//相差的分钟数:9292182
System.out.println("相差的秒数:"+duration2.toSeconds());//相差的秒数:9292
}
Period
:用于计算两个LocalDate
日期间的时长间隔
实例:
xxxxxxxxxx
/**
* 测试日期间隔Period类的API
* */
public void testPeriod(){
LocalDate nowDate = LocalDate.now();
LocalDate date = LocalDate.of(1997, 7, 15);
Period period = Period.between(date, nowDate);
System.out.println("相差的年:"+period.getYears());//相差的年:26
System.out.println("相差的月:"+period.getMonths());//相差的月:7
System.out.println("相差的日:"+period.getDays());//相差的日:6
}
自定义调整时间,
TemporalAdjuster
类
TemporalAdjuster
作用:通过实现函数式接口TemporalAdjuster
的抽象方法adjustInto
实现传入一个时间类型的参数,返回一个经过调整的时间类型参数,传参和返回参数类型都是Temporal
类型,是LocalDateTime
的父类型引用
备注说明:JDK8自带了很多时间调整器,都是TemporalAdjuster
中的静态方法,比如调整到这个月的周几,这个月的第一天,下个月的第一天,下一年的第一天,最后一天等等,返回的也是TemporalAdjuster
类型的调整器,和自定义的调整器一样直接通过localDateTime.with(temporalAdjuster)
进行调用
实例:
xxxxxxxxxx
/**
* 测试TemporalAdjuster类,自定义调整时间
*
* */
public void testTemporalAdjust(){
LocalDateTime now = LocalDateTime.now();
//将目标日期调整到当前日期的"下个月的第一天",TemporalAdjuster是一个函数式接口,只有一个抽象方法
//Temporal adjustInto(Temporal temporal);
//Temporal是LocalDateTime的父类,传参传入的是当前需要调整的日期
TemporalAdjuster firstDayOfNextMonthAdjuster=temporal->{
//temporal是当前需要被调整的时间LocalDateTime对象
LocalDateTime curTime = (LocalDateTime) temporal;
//将时间调整到当前时间的下个月的第一天
return curTime.plusMonths(1).withDayOfMonth(1);
};
LocalDateTime newTime = now.with(firstDayOfNextMonthAdjuster);
System.out.println("newTime:"+newTime);
}
设置日期时间的时区
LocalDate
、LocalTime
、LcoalDateTime
是不带时区的类,带时区的类包括ZoneDate
、ZoneTime
、ZoneDateTime
;每个时区都有对应的ID,ID的格式为"区域/城市",如
Asia/Shanghai
等类
ZoneId
中包含所有的时区ID,通过APIZoneId.getAvailableZoneIds()
获取到时区ID的set
集合
实例:
xxxxxxxxxx
/**
* 测试不同时区的时间
* */
public void testZoneXxx(){
//1. 获取ZoneId类中的所有时区信息
//ZoneId.getAvailableZoneIds().forEach(System.out::println);
//2. LocalDateTime.now()获取计算机的当前时间
//东八区比标准时间早八个小时
LocalDateTime now = LocalDateTime.now();
System.out.println("now:"+now);
//now:2024-02-22T01:16:50.761063400
//3. ZonedDateTime.now(Clock.systemUTC())获取世界标准时间,即格林威治时间,也即英国时间
ZonedDateTime worldStandardTime = ZonedDateTime.now(Clock.systemUTC());
System.out.println("worldStandardTime:"+worldStandardTime);
//worldStandardTime:2024-02-21T17:16:50.762063600Z
//4. ZonedDateTime.now()使用计算机操作系统默认的时区创建时间
ZonedDateTime nowZoneOfManualSystem = ZonedDateTime.now();
System.out.println("nowZoneOfManualSystem:"+nowZoneOfManualSystem);
//nowZoneOfManualSystem:2024-02-22T01:16:50.762063600+08:00[Asia/Shanghai]
//5. 使用指定的时区创建当前时刻对应的时间
ZonedDateTime timeOfAmerica = ZonedDateTime.now(ZoneId.of("America/Vancouver"));
System.out.println("timeOfAmerica:"+timeOfAmerica);
//timeOfAmerica:2024-02-21T09:16:50.763062900-08:00[America/Vancouver]
//6. timeOfAmerica.withZoneSameInstant(ZoneId.of("Asia/Shanghai"))修改调用时间到对应时区的时间
ZonedDateTime timeOfChina = timeOfAmerica.withZoneSameInstant(ZoneId.of("Asia/Shanghai"));
System.out.println("timeOfChina:"+timeOfChina);
//timeOfChina:2024-02-22T01:22:38.090544700+08:00[Asia/Shanghai]
//7. timeOfAmerica.withZoneSameLocal(ZoneId.of("Asia/Shanghai"))只修改时区,不修改时间
ZonedDateTime timeToChina = timeOfAmerica.withZoneSameLocal(ZoneId.of("Asia/Shanghai"));
System.out.println("timeToChina:"+timeToChina);
//timeToChina:2024-02-21T09:25:39.409325700+08:00[Asia/Shanghai]
}
注解是JDK5引入的,注解有个比较大的问题,一个注解不能再同一个位置重复使用,比如注解嵌套这个事情实现比较麻烦,JDK8引入重复注解的概念,允许在相同地方重复使用使用元注解@Repeatable注解定义的重复注解
重复注解定义和解析
实例:
xxxxxxxxxx
/**
* 配置并解析重复注解
* 通过反射获取注解并解析
* c1
* c2
* c3
* m1
* m2
* m3
* */
//配置在类上,注意是配置重复的注解,不是配置重复注解容器
"c1") (
"c2") (
"c3") (
public class TestJDK8Annotation {
//配置在方法上
"m1") (
"m2") (
"m3") (
public void testParseAnnotation() throws NoSuchMethodException {
//解析获取类上的指定注解,这个getAnnotationsByType方法貌似和老的获取注解的方法不同,留意一下
MyAnnotationJDK8[] repeatMyAnnotationOfClass = TestJDK8Annotation.class.getAnnotationsByType(MyAnnotationJDK8.class);
for (MyAnnotationJDK8 myAnnotation:repeatMyAnnotationOfClass) {
System.out.println(myAnnotation.value());
}
//获取方法上的指定注解,main方法上不能用这种方式来获取其上的注解,会抛异常
MyAnnotationJDK8[] repeatMyAnnotationOfMethod = TestJDK8Annotation.class.getMethod("testParseAnnotation").getAnnotationsByType(MyAnnotationJDK8.class);
for (MyAnnotationJDK8 myAnnotation:repeatMyAnnotationOfMethod) {
System.out.println(myAnnotation.value());
}
}
}
/**
* 定义重复注解容器注解
* 在属性中药定义注解类型数组
* */
RetentionPolicy.RUNTIME) (
@interface MyAnnotationJDK8s{
MyAnnotationJDK8[] value();
}
/**
* 定义一个可重复注解
* 需要在可重复注解中使用@Repeatable注解指定重复注解容器注解
*/
RetentionPolicy.RUNTIME) (
MyAnnotationJDK8s.class) (
@interface MyAnnotationJDK8 {
String value();
}
JDK8为
@Target
元注解新增了两种属性类型,
TYPE_PARAMETER
表示该注解能写在泛型上,类型参数的声明比如<T>
、<T extends Person>
,通俗的说就是标注了@Target(ElementType.TYPE_PARAMETER)
的注解既可以使用在泛型的前面,注意不能使用在类前面
TYPE_USE
表示定义中标注了@Target(ElementType.TYPE_USE)
的注解可以在任何用到类型的地方使用
TYPE_PARAMETER
实例
xxxxxxxxxx
/**
* 测试@Target(ElementType.TYPE_PARAMETER)
* 标注了@Target(ElementType.TYPE_PARAMETER)特定属性值的@Target注解可以使用在类或者方法的泛型前面
* 是不是很像SpringBoot中的参数类型前面的注解
* */
public class TestTypeAnnotation < T>{
public < E extends Integer> void testTypeAnnotation(){
}
}
ElementType.TYPE_PARAMETER) (
@interface ParamType{
}
TYPE_USE
实例
定义中标注了
@Target(ElementType.TYPE_USE)
的注解可以在任何用到类型的地方使用
xxxxxxxxxx
public class TestTypeAnnotation < T>{
//只要是类型的前面就能添加定义标注了@Target(ElementType.TYPE_USE)的注解
private int a=10;
public void testTargetFiled( String str, int a){
//局部变量前面也可以加
int x=1;
}
}
ElementType.TYPE_USE) (
@interface NotNull{
}
原来的缺点
在Java8及以前,java中用到的自带的类【String、集合等】都在JDK/jre/lib/rt.jar
文件夹中,这个目录下的类的包名就是java.xxx.Xxx
了,这样会使java的运行显得臃肿和笨重,例如执行代码String str="hello"
,类加载器需要去jre
的rt.jar
包下大量搜索对应类的文件并加载到JVM中,很多没用到的类都一股脑塞到jre
目录下,对JVM管理来说增加非必要负担和性能消耗,Java9模块化可以按需自定义java平台提供的类,所以jdk9的文件夹下没有jre
目录
模块化能由于按需自定义java平台类,减少内存开销、提高效率
权限修饰符只能修饰类、成员变量、成员方法,不能对包使用,很多包可能用户根本不需要,而是程序内部的运行需要,且第三方只要导入项目就能完整看到所有的源代码,还能直接使用这些类,JDK9能对包进行隐藏,同时隐藏包中的所有类
强封装,每一个模块都声明哪些包是公开的,哪些类是内部使用的,java编译和运行通过实施这些规则来确保外部模块无法使用内部类型
实例
需求:创建一个ModuleA
模块,在ModuleB
模块进行访问
核心就是在输出模块和输入模块使用一个
module-info.java
的文件来定义模块输出规则和模块输入规则
创建一个ModuleA
,然后创建两个包,com.itheima.utils
和com.itheima.model
在utils
包中创建一个ArrayUtils
工具类并创建一个获取数组最大值的方法
在modle
包中创建一个Person
类
新建一个输出模块信息,只是输出utils
包,model
包对外隐藏
src目录右键
-- new
-- module-info.java
【模块信息定义文件】 --在名为module-info.java
的文件中定义模块输出规则
就是在src目录下创建一个名为
module-info.java
的文件,在文件中指明模块对外输出的包
模块输出规则
文件中这个模块名字不一定要和真实的模块名字相同,要输出哪个包就
exports ...
,以下表示只输出utils
包
xxxxxxxxxx
module moduleA{
exports com.itheima.utils;
}
创建一个输入模块信息,和上一个是一样的,创建module-info.java
文件,配置模块输入规则
但是此时moduleA会报错,因为代码中还没有用import引入对应的类的包名
xxxxxxxxxx
module moduleB{
requires moduleA;
}
创建一个ModuleB
,然后新建一个ModuleTest
类,测试使用ArrayUtils
此时就能正常使用模块a中的utils包下的类了,而且不需要像以前一样对模块打包通过手动导入或者通过maven导入就能直接跨模块使用一个类
优点总结
模块化,凡是没有输出的模块或者没有引入的模块对应的包和类都不会对应导入当前模块
jdk9
使用jshell
工具实现的交互式编程,交互式编程的作用是让开发人员马上看到代码的运行效果
交互式编程
java代码的编程模式是:编辑、保存、编译、运行、调试;开发过程中想要实时看到结果原来需要把代码块单独写到main方法中运行,降低开发效率
交互式编程指不需要编写类、直接声明变量、方法或者执行语句就能不通过编译马上看到运行结果
jshell工具的使用
jdk9中的bin目录下自带jshell工具jshell.exe
,使用该工具需要确保当前系统环境是jdk9以上的版本,使用命令java -version
能查看当前系统的jdk版本
在环境变量配置合理的情况下CMD窗口使用命令jshell
能直接打开该工具,
可以在该工具中定义变量,写一句就会执行一句,且可以定义方法,方法定义以后可以直接通过方法名调用,直接方法名就可以调用,实例方法都不需要对象调用,有点像js,就是声明了有这个东西,可以直接拧出来用,且实时展示效果
用java代码定义变量和方法
像js一样直接通过方法名对方法进行调用
/list
命令是显示当前窗口已经正确执行过的代码
/methods
命令是查看当前窗口已经定义过的方法
/var
命令是查看当前窗口已经定义过的变量
/edit
命令是打开一个类似文本编辑器的窗口界面,里面包含当前窗口已经正确执行过的java语句并且可以在其中继续编辑后续语句,编辑完成后点击accept
【一定要点击才能生效】就能在CMD窗口中自动执行并显示执行效果,窗口中的语句编辑可以换行
/open 代码块绝对路径
命令可以执行文件中的定义变量和方法以及调用方法的语句,课堂演示文件名后缀还是使用的.java
,相当于引入了外部以.java作为后缀的文件内容到内部编辑器中
/imports
命令可以查看当前java默认导入的包
/imports java.utils.*
可以额外导入没有导入的包,这里utils包实际已经导入,只是作为演示,这个包是否必须在某个目录下?
/exit
命令是退出jshell
关注接口中方法私有化的作用以及如何定义调用私有化方法
接口方法私有化
意义:当接口中的多个默认方法和静态方法中部分代码块多次重复,将这部分代码封装成私有方法供接口内部使用,解决默认方法和静态方法的代码重复冗余问题【因为接口中只有默认方法和静态方法才有方法体】
实例:
xxxxxxxxxx
public interface UserDao {
default void methodA(){
System.out.println("methodA执行");
commons();
}
default void methodB(){
System.out.println("methodB执行");
commons();
}
//定义一个私有方法,把重复部分的代码抽离出来,在methodA和methodB方法中去调用
//私有方法只能在本类中调用,这里包括了接口的实现类也不能直接调用,只能通过继承的静态方法和默认方法自动调用
private void commons(){
System.out.println("A...");
System.out.println("B...");
System.out.println("C...");
};
}
JDK7及以前传统释放资源的代码
实例
存在问题:释放资源的代码非常繁杂;而且现在只有一个流,如果流非常多,关闭资源的代码甚至会超过正常的业务代码
xxxxxxxxxx
public static void main(String[] args) {
FileInputStream fileInputStream=null;
try{
//1. 建立程序与文件的数据通道
fileInputStream=new FileInputStream("E:\\JavaStudy\\project\\blog\\vuePressBlog.md");
//2. 创建字节数组缓冲区
byte[] buffer = new byte[1024];
//3. 读取数据并输出
int length=0;
while((length=fileInputStream.read(buffer))!=0){
System.out.println(new String(buffer,0,length));
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally {
//4. 这个if中的代码块就是释放资源的代码
if(fileInputStream!=null){
try{
fileInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
JDK8释放资源的代码
JDK8声明和初始化的代码必须在try后面的小括号中流会自动释放
实例
xxxxxxxxxx
public static void main(String[] args) {
//JDK8中需要释放资源的对象定义在小括号中表示代码执行完就释放掉该资源,并且注意初始化流对象的代码也一定要写在小括号中,小括号只声明不赋值或者赋值null是不行的,编译会报错,流对象声明和初始化必须在一起
try(FileInputStream fileInputStream=new FileInputStream("E:\\JavaStudy\\project\\blog\\vuePressBlog.md")){
//2. 创建字节数组缓冲区
byte[] buffer = new byte[1024];
//3. 读取数据并输出
int length=0;
while((length=fileInputStream.read(buffer))!=0){
System.out.println(new String(buffer,0,length));
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
JDK9释放资源的代码
JDK9流的声明和初始化可以放在try语句块外面,只需要在try语句块后面的小括号中传递流对象进去即可
实例
xxxxxxxxxx
public static void main(String[] args) {
FileInputStream fileInputStream=new FileInputStream("E:\\JavaStudy\\project\\blog\\vuePressBlog.md")
//JDK9只需要在try后面的小括号中传入流对象,使用后就能自动关闭
try(fileInputStream){
//2. 创建字节数组缓冲区
byte[] buffer = new byte[1024];
//3. 读取数据并输出
int length=0;
while((length=fileInputStream.read(buffer))!=0){
System.out.println(new String(buffer,0,length));
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
优化点
JDK8即JDK8以前,下划线_
是可以作为一个标识符单独在命名中使用的,但是JDK9中不再允许单独使用_
作为标识符进行命名了,JDK9会编译报错
String类内部底层由字符数组变成了字节数组,目的是为了节省内存空间
以前的String类
卧槽还真特么是,这不给他面试安排上
String类将字符串存储在char数组中,每个字符存储使用两个字节,但是从实际应用数据表明,堆内存中主要就是字符串,大多数字符串对象只包含拉丁-1字符,这样的字符只需要使用一个字节的存储空间,会导致这样的字符串对象内部字符数组的一般空间被闲置
以前的字符串对象实际上是一个char数组private final char value[]
JDK9以后得String对象的底层变成一个byte数组private final byte[] value
由于String类的变化,现在StringBuilder
、StringBuffer
和StringBuilder
的父类AbstractStringBuilder
以及字符串相关的类Hotspot
和VM
内部字符串串都采用byte数组了
JDK9中新增了快速创建只读集合的方法,
集合【List、Set、Map】中新增静态方法of()
可以将不同数量的参数处理返回得到只读的List、Set、Map集合
注意Map的key和value似乎有多种参数类型选择
这种方法创建的只读集合不支持增删改元素,运行时会抛不支持操作异常
创建只读集合实例:
xxxxxxxxxx
public static void main(String[] args) {
//1. 创建一个List的只读集合
List<String> list = List.of("张三", "李四", "王五");
System.out.println("list:"+list);//list:[张三, 李四, 王五]
//2. 创建一个Set的只读集合
Set<String> set = Set.of("张三", "李四", "王五");
System.out.println("set:"+set);//set:[王五, 李四, 张三]
//3. 创建一个Map集合
Map<Integer, String> map = Map.of(1, "张三", 2, "李四", 3, "王五");
System.out.println("map:"+map);//map:{3=王五, 2=李四, 1=张三}
//尝试向只读集合添加元素
list.add("赵六");//UnsupportedOperationException
}
获取流中不小于50的前几个连续元素、删除流中不小于50的前几个连续元素、创建一个所有元素都可以为
null
的Stream
流
Stream<T> --> stream.takeWhile(predicate<? super Integer> predicate)
作用:从Stream流中依次获取满足条件的元素,只要遇到第一个不满足条件的元素就停止获取后续元素【后续存在满足条件的元素也不再获取】
实例:
xxxxxxxxxx
public void testTakeWhile(){
Stream<Integer> stream = Stream.of(10, 20, 30, 40, 50, 7, 60);
//获取流中不小于50的前几个连续元素
stream.takeWhile(data->data<50).forEach(System.out::print);//10203040
}
Stream<T> --> stream.dropWhile(predicate<? super Integer> predicate)
作用:从Stream流中依次删除满足条件的元素,只要遇到第一个不满足条件的元素就停止删除后续元素【后续存在满足条件的元素也不再删除】
实例:
xxxxxxxxxx
public void testDropWhile(){
Stream<Integer> stream = Stream.of(10, 20, 30, 40, 50, 7, 60);
//删除流中不小于50的前几个连续元素
stream.dropWhile(data->data<50).forEach(System.out::print);//50760
}
Stream<Object> --> Stream.ofNullable(null)
作用:创建一个所有元素都可以为null
的Stream
流
备注说明:JDK8中的Stream不能完全为null,可以存在个别元素是null,否则会报空指针异常;JDK9允许创建元素全为null的Stream流,但是使用count方法统计元素个数仍然显示为0
实例:
xxxxxxxxxx
public void testOfNullStream(){
//创建一个元素全为null的stream流
Stream<Object> stream = Stream.ofNullable(null);
System.out.println(stream.count());//0
}
这套API几乎不用,也基本没讲
java.awt.image
包下新增支持多分辨率图片的API,实现还是将不同分辨率的图片封装到一张多分辨率的图像中,通过接口的getResolutionVariant
方法根据客户端的屏幕大小获取不同分辨率的图像【手机屏幕的大小各个厂商是不同的】
使用浏览器可以发送HTTP请求给网络服务器,使用CURL也可以直接从CMD窗口发送HTTP请求,使用
HTTPClient
可以直接在程序中发起Http请求,JDK9以前通过HttpURLConnection
来获取网络资源,相比于HttpURLConnection
只支持HTTP1.0,HTTPClient
还支持WebSocket
和HTTP2.0
使用HttpClient
访问百度服务器得到百度首页
HttpClient
的构造方法被私有化了,需要通过静态方法调用;实际这里讲的很水,HttpClient可以专门来学习一下
实例
xxxxxxxxxx
public void testHttpClient() throws URISyntaxException, IOException, InterruptedException {
//1. 创建HttpClient对象
//HttpClient的构造方法是被私有化的
HttpClient httpClient = HttpClient.newHttpClient();
//2. 创建请求的构造器
HttpRequest.Builder builder = HttpRequest.newBuilder(new URI("https://www.baidu.com"));
//3. 使用请求构造器构建请求,并设置请求参数
HttpRequest request = builder.header("user-agent", "Chrome").GET().build();
//4. 使用HttpClient发送请求,并得到响应的对象,
//这种用法在JDK9中可以用,在JDK11就被改了,HttpResponse.BodyHandler.asString()变更为HttpResponse.BodyHandlers.ofString()
//意思是用响应处理器把响应结果处理成字符串
//HttpResponse<Object> response = httpClient.send(request, HttpResponse.BodyHandler.asString());
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
//5. 查看响应对象信息
System.out.println("响应状态码:"+response.statusCode());//响应状态码:200
System.out.println("响应的信息:"+response.body());//响应的信息:<!DOCTYPE html><!--STATUS OK--><html><head><meta h...
}
HttpClient的使用需要声明输入jdk.incubator.httpclient
模块,默认情况下该模块是不能重classpath中获取,需要在module-info.java
文件中对对应模块进行引入
我这里使用的是jdk17版本,应该是有修改,没有配置似乎还是找到了对应的内容并成功响应信息,处理响应内容也使用11中更新的API
配置实例
jdk9使用
HttpClient
需要引入的模块
xxxxxxxxxx
module jdk9{
requires jdk.incubator.httpclient;
}
JDK9废弃了几个不太常用的功能,主要废弃了Applet API,用于java编写小应用程序,可以直接嵌入到网页,但是现在很少使用java开发客户端,这个类已经很少使用了,而且由于安全问题,主流浏览器取消了对Java浏览器插件的支持【Applet就是用来做浏览器插件的】,HTML5的出现也加速了这个过程
jdk8生成java帮助文档使用HTML4标准,jdk9使用javadoc生成帮助文档时支持HTML5标准,与HTML4的区别是HTML5支持搜索,JDK8的帮助文档没有搜索框,找类需要按包和类进行索引,JDK9的帮助文档有了搜索框,可以直接在搜索框搜索类,这是H5的强大特性【感觉是一个标准,JDK9支持HTML5标准,javadoc生成的帮助文档出现了搜索框】
动态编译的目的是为了提高编译效率,
sjavac
【smarter java compilation】在jdk8添加的初级版本,在JDK9对稳定和可靠性进行了优化,能够用来编译任意大型的java项目,sjavac
在javac
的基础上扩展了增量编译【只编译必要内容,不会执行编译所有的东西就去进行编译】和并行编译【编译期间使用多个核心即并发编译】但是这个工具没有随OpenJDK一起提供,必须访问
http://hg.openjdk.java.net/jsk9/dev/langtools
进行获取,当时是因为还没有成熟尚未发布,还在持续升级,现在不知道是不是随OpenJDK一起提供的,后面的JDK新特性再关注
语法上JDK8以前不允许约束泛型的尖括号和匿名内部类一起使用,语法上会报错【我这里使用1.8.0_101仍然会编译报错,弹幕说新版JDK8支持这种写法,未验证】
JDK9以后就支持这种写法了
xxxxxxxxxx
//jdk9之前该语句是会报错的,jdk9开始是合法的
List<String> list = new ArrayList<>(){};
list.add("aa");
list.add("bb");
list.add("cc");
System.out.println("集合的元素:"+ list);
JDK10以前定义变量存在的问题
以前的变量声明定义必须在变量前声明变量类型
有些流行的语言可以自动进行类型推断,比如JS
xxxxxxxxxx
String str = "abc";
long l = 10L;
boolean b = true;
ArrayList<String> list = new ArrayList();
Stream<String> stream = list.stream();
JDK10的局部变量类型推断
语法精简,使用var声明的局部变量会根据右边的字面值推断左边的类型,编译过程会自动将类型推断出来,实际的字节码效果和旧版本效果是一致的
xxxxxxxxxx
var str = "abc"; // 推断为 字符串类型
var l = 10L; // 推断为long 类型
var flag = true; // 推断为 boolean 类型
var list = new ArrayList<String>(); // 推断为 ArrayList<String>,注意因为var中没有泛型,需要在初始化创建的时候指定泛型为String
var stream = list.stream(); // 推断为 Stream<String>
局部变量类型自动推断使用场景
可以使用的场景
局部变量
循环中的变量
不能使用的场景
成员变量
方法参数
方法体的返回值类型不能用【即返回值类型不能写var】
注意事项
var不是关键字,只是一个保留字,意味着var可以作为类名、方法名...,但是不建议这么使用
var定义的变量必须立马赋值,否则无法在编译阶段进行类型推断,编译阶段也无法判断后续是否有赋值,赋值什么类型
var不能像类型一样同时定义多个变量
JDK10以前完整代码库被分解到多个仓库中,这种多仓库分布在代码管理操作的管理方面做的很差,特别是不可能在相互依赖的变更集的仓库间原子性提交操作变更,比如某个错误修复的代码跨越了多个仓库,无法原子性地对两个仓库进行修改,这种跨越多个存储库的更改是比较常见的,一个仓库就比较容易做到操作原子性提交
这个接口不是给开发者用的,是方便JDK的开发人员在JVM源码中快速集成和移除垃圾回收器
此前垃圾回收器的代码分散,不方便新增垃圾回收器也不利于移除现有垃圾回收器,通过引入一个干净的垃圾回收器接口GC来改善不同垃圾回收器相互之间的隔离,方便后续新增或者移除垃圾回收器,目的是让JVM内部得垃圾回收器代码更好模块化、在不影响当前代码库的情况下方便地向JVM中添加或者移除垃圾回收器
G1回收器引入并行Full GC
G1 是设计来作为一种低延时的垃圾回收器。G1收集器还可以进行非常精确地对停顿进行控制。从JDK7开始启用G1垃圾回收器,在JDK9中G1成为默认垃圾回收策略。截止到ava 9,G1的Full GC采用的是单线程算法。也就是说G1在发生Full GC时会严重影响性能 ,JDK10对G1进行了提升,引入了并行Full GC算法, 发生Full GC时可以使用多个线程进行并行回收,用户体验更好
JVM性能增强功能,开发人员无法直接使用
JDK5引入了类数据共享,当多个JVM用到相同的类文件时可以将一组类预处理为共享的存档文件,在运行时将共享存档文件进行内存映射减少启动时间,多个JVM共享同一份存档文件还能减少动态内存占用,但是JDK5只支持引导类加载器加载归档的类
JDK10对应用程序类数据共享进行扩展,允许"应用程序类加载器"、该加载器内置了平台类加载器和自定义类加载器加载已经归档的类
自己定义的类也可以在JVM中进行共享
作用
允许不同Java进程间共享通用的类元数据来减少内存占用空间
缩短程序的启动时间
JVM性能增强功能,开发人员无法直接使用
Safepoint
【安全点】是Hotpot JVM
中让应用程序中所有线程停止的机制,为了做一些非常安全的事情需要让所有线程停下来等待该安全操作执行,比如菜市场人来人往,突然需要清点人数,此时需要让人都不动,方便统计,安全点的作用也是如此
实现原理是JVM设置一个全局的状态值,应用线程去观察该值,一旦发现JVM将该值设置为所有应用线程停止的状态,就会各自选择一个点停下来,该点就叫安全点,等待所有的应用线程都停下来后,JVM就会开始做一些安全级别非常高的事情【如垃圾清理暂停、类的热更新,偏向锁的取消以及IDE的各种debug操作】
但是让所有线程都到就近的safepoint停下来需要较长时间且让所有线程都停下来显得比较粗暴
JDK10引入线程本地握手【Thread Local Handshake】,线程本地握手在JVM内部是相当低级别的更改,修改了安全点机制允许在不运行全局虚拟机安全点的情况下实现线程回调,使得部分回调操作只需要停掉单个线程而不是停止所有线程
JVM性能增强功能,开发人员无法直接使用
现在计算机配置的内存一般都是DRAM【动态随机读取存储器】,速度快但是价格昂贵;近些年出现了NVDIMM【非易失性双列直插式内存模块】,价格便宜,速度比DRAM慢,断电能保留数据,随着NVDIMM廉价内存的可用性,未来系统可能配备异构内存架构【即多种内存】,除了DRAM外还会配备一种或者多种如NVDIMM类型的内存存储器
该特性的目标是不更改现有应用程序代码的情况下,将以前的DRAM中的堆内存移动到NVDIMM中,其他的内存结构如代码堆、元空间、线程堆栈仍然继续驻留在DRAM中【即使用不频繁的内存放在更便宜的NVDIMM中】
多JVM部署时,一台机器部署多台JVM,一些JVM【守护程序等服务】的优先级低于其他JVM,低优先级进程可以将堆内存放在NVDIMM中,以牺牲访问延迟的代价允许高优先级进程使用更多的DRAM
大数据和内存数据库等应用程序对内存的需求不断增长,这样的应用程序可以将堆内存放在NVDIMM中,与DRAM相比容量更大,成本更低
JVM性能增强功能,开发人员无法直接使用
java程序的整个流程
Java编译器指的是JDK自带的javac指令。这一指令可将Java源程序编译成.class字节码文件(bytecode)。字节码无法直接运行,但可以被不同平台JVM中的 interpreter(解释器) 解释形成微处理指令才能执行。由于一个Java指令可能被转译成十几或几十个对等的微处理器指令,这种解释执行模式执行的速度相当缓慢。
JIT编辑器
由于interpreter效率低下,JVM中又增加JIT compiler(即时编译器,just in time)会在运行时有选择性地将运行次数较多的方法直接编译成二进制代码【不再编译成字节码】,直接运行在底层硬件上。花费少许的编译时间来节省稍后相当长的解释执行时间
JIT这种设计的确增加不少效率,但是它并未达到最顶尖的效能,因为某些极少执行到的Java指令在JIT编译时所额外花费的时间可能比interpreter解释器执行时的时间还长,针对这些指令而言,整体花费的时间并没有减少。
就是JIT编辑器编译二进制的时间比解释执行时间长,用的少的代码反而会消耗更多的时间,对于选择哪些代码直接编译成二进制的算法还在研发阶段
Graal是基于Java的JIT编译器,这项 JEP 将 Graal 编译器研究项目引入到 JDK 中。
为了让 JVM 性能与当前 C++ 所写版本匹敌(或有幸超越)提供基础。
java可以直接调用c语言或者c++语言写的代码,但是需要一个头文件,javah是用于生成C语言的头文件,JDK8开始,javah的功能已经集成到javac中,所以在JDK10中去掉了该工具
实例
xxxxxxxxxx
public class Helloworld{
//native修饰的方法是本地方法,实质就是去调用C语言和C++的方法,使用该方法需要生成对应方法需要的头文件
public native void sayHello();
}
使用命令javac -h . Helloworld.java
对该调用了c语言和c++语言的类进行编译
编译后发现当前目录除了字节码文件还多出来一个.h结尾的头文件,在头文件中生成了一个当前类对应方法的签名
此前Unicode语言环境扩展仅限日历和数字,该新特性在JDK类中实现了最新规范添加了更多扩展,包括:
cu
货币类型
fw
一周的第一天
rg
区域覆盖
tz
时区
为了支持这些附加扩展,JDK还对以下API进行更改:
java.text.DateFormat::getInstance
将根据扩展名返回实例 ca , rg 和/或 tz
java.text.DateFormatSymbols::getInstance
将根据扩展名返回实例 rg
java.text.DecimalFormatSymbols::getInstance
将根据扩展名返回实例 rg
java.text.NumberFormat::get*Instance
将根据扩展名 nu 和/或返回实例 rg
java.time.format.DateTimeFormatter::localizedBy
将返回 DateTimeFormatter 基于扩展情况下 ca ,rg 和/或 tz
java.time.format.DateTimeFormatterBuilder::getLocalizedDateTimePattern
将根据 rg 扩展名返回模式字符串。
java.time.format.DecimalStyle::of
将 DecimalStyle 根据扩展名返回实例 nu ,和/或 rg
java.time.temporal.WeekFields::of
将 WeekFields 根据扩展名 fw 和/或返回实例 rg
java.util.Calendar::{getFirstDayOfWeek,getMinimalDaysInWeek}
将根据扩展名 fw 和/或返回值 rg
java.util.Currency::getInstance
将 Currency 根据扩展名 cu 和/或返回实例 rg
java.util.Locale::getDisplayName
将返回一个字符串,其中包括这些U扩展名的显示名称
java.util.spi.LocaleNameProvider
这些U扩展的键和类型将具有新的SPI
Open JDK源代码中的密钥库当前为空。因此,默认情况下,TLS等关键安全组件在Open JDK构建中不起作用【感觉就是OracleJDK中的一些组件在OpenJDK中用不了】。JDK10开源Oracle Java SE Root CA程序中的根证书,减少这些构建与Oracle JDK构建之间的差异。
JDK9以后每隔六个月就会严格发布JavaSE和JDK的新版本,之前的命名方案不适合
JDK9以前的版本命名方式
在jdk的bin目录使用命令./java -version
能查看当前所在目录jdk的版本信息,以前的版本信息是不带发布时间的,JDK9以后得发布版本带时间信息,如18.9表示2018年9月份,还会显示具体的时间如2018-09-25
JDK10给集合List、Set、Map中新增一个静态方法copyOf,可以复制一个集合中的元素到另一个只读集合中去
实例:
xxxxxxxxxx
public void testCopyOf(){
ArrayList<String> list = new ArrayList<>();
list.add("aa");
list.add("bb");
list.add("cc");
List<String> copyList = List.copyOf(list);
for (String str:copyList) {
System.out.print(str);//aabbcc
}
System.out.println(copyList.getClass());//java.util.ImmutableCollections$ListN是一个内部类
//匿名内部类也可以在IDEA中双击shift直接搜索
// copyList.add("dd");//UnsupportedOperationException
}
只读集合的实现原理就是对应的方法直接抛异常
重写集合中的增删改查方法,直接抛异常,即让集合不能增删改
xxxxxxxxxx
internal.ValueBased .
static abstract class AbstractImmutableCollection<E> extends AbstractCollection<E> {
// all mutating methods throw UnsupportedOperationException
public boolean add(E e) { throw uoe(); }
public boolean addAll(Collection<? extends E> c) { throw uoe(); }
public void clear() { throw uoe(); }
public boolean remove(Object o) { throw uoe(); }
public boolean removeAll(Collection<?> c) { throw uoe(); }
public boolean removeIf(Predicate<? super E> filter) { throw uoe(); }
public boolean retainAll(Collection<?> c) { throw uoe(); }
}
以前使用IO流复制文件非常麻烦
使用IO流复制文件实例
实例
文件的相对路径以当前模块作为相对路径的根路径,缺点是流需要手动关闭且循环读写代码写起来比较麻烦
xxxxxxxxxx
public void testIOCopyFile() throws IOException {
FileReader fileReader = new FileReader("file/a.txt");
FileWriter fileWriter = new FileWriter("file/b.txt");
//使用char数组循环读写
char[] chars=new char[1024];
int length;
//当读到流中有内容就直接输出到文件b.txt中
while((length=fileReader.read(chars))!=-1){
fileWriter.write(chars,0,length);
}
fileReader.close();
fileWriter.close();
}
使用TransferTo方法复制文件
实例
这个还是需要自己关闭流,就是省了中间的循环读写代码
xxxxxxxxxx
public void testTransferTo() throws IOException {
FileReader fileReader = new FileReader("file/a.txt");
FileWriter fileWriter = new FileWriter("file/c.txt");
//将输入流的数据写到输出流中,方便用户做文件复制
fileReader.transferTo(fileWriter);
fileReader.close();
fileWriter.close();
}
源码
和循环读写的代码差不多,多了空检测
xxxxxxxxxx
public long transferTo(Writer out) throws IOException {
Objects.requireNonNull(out, "out");
long transferred = 0;
char[] buffer = new char[TRANSFER_BUFFER_SIZE];
int nRead;
while ((nRead = read(buffer, 0, TRANSFER_BUFFER_SIZE)) >= 0) {
out.write(buffer, 0, nRead);
transferred += nRead;
}
return transferred;
}
IO流家族中的PrintStream、PrintWriter、Scannert的构造方法添加了Charset参数,通过charset可以指定IO流操作文本时的编码
IDEA中默认使用的UTF-8编码,在IDEA中使用输出流输出UTF-8编码格式的字符串,内容正常显示,说明PrintStream的默认编码格式也是utf-8
实例
xxxxxxxxxx
public void testPrintStream() throws FileNotFoundException {
PrintStream printStream = new PrintStream("file/d.txt");
printStream.println("你好中国");
printStream.close();
}
需求,如果此时希望使用GBK编码来打印内容
Charset是一个抽象类,不能直接构造对象,通过
Charset.forName("gbk")
给定编码来获取charset对象此时会发现数据乱码,因为IDEA默认是UTF-8,但是输出流使用的是GBK,不会自动转换,所以文件解析发生乱码,记事本打开文件另存为的编码属性编码格式ANSI表示使用电脑默认的编码方法,操作系统一般简体中文使用的是GBK的编码格式
xxxxxxxxxx
public void testCharset() throws IOException {
PrintStream printStream = new PrintStream("file/ps.txt", Charset.forName("gbk"));
printStream.println("您好中国");//�����й�
printStream.close();
}
无参的toString方法早就有了,这里新增的是
toString(Charset charset)
,确保将byte数组转成字符串的时候编码格式与bytes数组的编码格式一致
xxxxxxxxxx
public void testByteArrayStream() throws IOException {
String str="你好";
//byte[] bytes = str.getBytes();//默认使用编码格式UTF-8将字符串转换成字节数组
//将字符串转成字节数组也可以指定编码格式
byte[] bytes = str.getBytes(Charset.forName("gbk"));//使用指定GBK编码将字符串转成字节数组
System.out.println(Arrays.toString(bytes));//[-28, -67, -96, -27, -91, -67]
//转成GBK字节数组变成[-60, -29, -70, -61]
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);//这是将字节数组的内容写到输入流对象中
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();//输出流没有目的地是直接写在内存中
int length;
while((length=byteArrayInputStream.read())!=-1){
byteArrayOutputStream.write(length);
}
//没有指定编码格式默认也是使用UTF-8将字节数组转成字符串
//toString不带参数是很早就有的方法,toString带编码格式是JDK10的新特性
//System.out.println(byteArrayOutputStream.toString());//你好
//上诉代码直接默认用UTF-8的编码格式输出GBK编码格式的字节数组会乱码,下面相应的需要使用GBK的编码格式
System.out.println(byteArrayOutputStream.toString(Charset.forName("GBK")));//你好
byteArrayInputStream.close();
byteArrayOutputStream.close();
}
Arrays.toString(arr)
作用是打印一个数组的信息,arr.toString()
str.startWith("char")
作用是判断字符串是否以某个字符char
或者某个子串char
打头
一般开发使用的是OracleJDK
,OracleJDK
在部分版本上是收费的,但是java8是免费的,2019年以后得OracleJDK是收费的,现在应该有变化,学习Oracle JDK的新特性只需要去学习OpenJDK的新特性即可
java历史
1991年Sun公司推出希望能在各种各种消费型电子产品【机顶盒、冰箱、收音机等】上运行的程序框架Oak,1995年互联网潮流兴起,Oak找到适合发展的市场定位【95年以前以静态网页为主,95年以后动态网页需求激增】
1995年JDK Beta,有很多bug
1996.01--JDK1.0,真正稳定的版本是JDK1.0.2,也被称为Java1【Oak改为Java是因为Oak已经被注册了】
1998年,JDK分为三个版本,应用于桌面环境的J2SE;应用于移动、无线和有限资源情况下的J2ME;应用于基于Java的应用服务器开发的J2EE,J2ME和J2EE是基于J2SE的
2000年,J2SE1.3
2002年,J2SE1.4
2004年,J2SE5.0【推出枚举、泛型、自动装箱拆箱】
2006年,Java SE6
2011年,JavaSE7
2014年,JavaSE8【LST】
2017年,JavaSE9
2018年3月,JavaSE10
2018年9月,JavaSE11【LST】
2019年3月,JavaSE12
2019年9月,JavaSE13
OpenJDK的官网中http://openjdk.java.net/
的JEP【JDK Enhancement Proposals】JDK增强建议就是JDK的新特性,进入OpenJDK的官网点击JEP Process就能找到各个版本的JDK新特性,也可以在该页面找到JDK版本点进去Features菜单就是JDK新版本的所有特性
关注构建特定元素的流的用法IntStream intStream = IntStream.range(0,10);
,这种构建方式黑马的新特性课程中没有讲
关注流式用法stream.filter(Objects::nonNull)
即过滤出流中不为null的元素,关注一下Objects类中的相关方法